「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

linux-xiaofeng發表於2020-11-25

我們通俗一點講:
Level_triggered(水平觸發):當被監控的檔案描述符上有可讀寫事件發生時,epoll_wait()會通知處理程式去讀寫。如果這次沒有把資料一次性全部讀寫完(如讀寫緩衝區太小),那麼下次呼叫 epoll_wait()時,它還會通知你在上沒讀寫完的檔案描述符上繼續讀寫,當然如果你一直不去讀寫,它會一直通知你!!!如果系統中有大量你不需要讀寫的就緒檔案描述符,而它們每次都會返回,這樣會大大降低處理程式檢索自己關心的就緒檔案描述符的效率!!!
Edge_triggered(邊緣觸發):當被監控的檔案描述符上有可讀寫事件發生時,epoll_wait()會通知處理程式去讀寫。如果這次沒有把資料全部讀寫完(如讀寫緩衝區太小),那麼下次呼叫epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該檔案描述符上出現第二次可讀寫事件才會通知你!!!這種模式比水平觸發效率高,系統不會充斥大量你不關心的就緒檔案描述符!!!
阻塞IO:當你去讀一個阻塞的檔案描述符時,如果在該檔案描述符上沒有資料可讀,那麼它會一直阻塞(通俗一點就是一直卡在呼叫函式那裡),直到有資料可讀。當你去寫一個阻塞的檔案描述符時,如果在該檔案描述符上沒有空間(通常是緩衝區)可寫,那麼它會一直阻塞,直到有空間可寫。以上的讀和寫我們統一指在某個檔案描述符進行的操作,不單單指真正的讀資料,寫資料,還包括接收連線accept(),發起連線connect()等操作…
非阻塞IO:當你去讀寫一個非阻塞的檔案描述符時,不管可不可以讀寫,它都會立即返回,返回成功說明讀寫操作完成了,返回失敗會設定相應errno狀態碼,根據這個errno可以進一步執行其他處理。它不會像阻塞IO那樣,卡在那裡不動!!!

select(),poll()模型都是水平觸發模式,訊號驅動IO是邊緣觸發模式,epoll()模型即支援水平觸發,也支援邊緣觸發,預設是水平觸發。
這裡我們要探討epoll()的水平觸發和邊緣觸發,以及阻塞IO和非阻塞IO對它們的影響!!!下面稱水平觸發為LT,邊緣觸發為ET。
對於監聽的socket檔案描述符我們用sockfd代替,對於accept()返回的檔案描述符(即要讀寫的檔案描述符)用connfd代替。
我們來驗證以下幾個內容:
1.水平觸發的非阻塞sockfd
2.邊緣觸發的非阻塞sockfd
3.水平觸發的阻塞connfd
4.水平觸發的非阻塞connfd
5.邊緣觸發的阻塞connfd
6.邊緣觸發的非阻塞connfd
以上沒有驗證阻塞的sockfd,因為epoll_wait()返回必定是已就緒的連線,設不設定阻塞accept()都會立即返回。例外:UNP裡面有個例子,在BSD上,使用select()模型。設定阻塞的監聽sockfd時,當客戶端發起連線請求,由於伺服器繁忙沒有來得及accept(),此時客戶端自己又斷開,當伺服器到達accept()時,會出現阻塞。本機測試epoll()模型沒有出現這種情況,我們就暫且忽略這種情況!!!

需要C/C++ Linux伺服器架構師學習資料加群812855908(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

檔名:epoll_lt_et.c

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <errno.h>
 5 #include <unistd.h>
 6 #include <fcntl.h>
 7 #include <arpa/inet.h>
 8 #include <netinet/in.h>
 9 #include <sys/socket.h>
 10 #include <sys/epoll.h>
 11 
 12 /* 最大快取區大小 */
 13 #define MAX_BUFFER_SIZE 5
 14 /* epoll最大監聽數 */
 15 #define MAX_EPOLL_EVENTS 20
 16 /* LT模式 */
 17 #define EPOLL_LT 0
 18 /* ET模式 */
 19 #define EPOLL_ET 1
 20 /* 檔案描述符設定阻塞 */
 21 #define FD_BLOCK 0
 22 /* 檔案描述符設定非阻塞 */
 23 #define FD_NONBLOCK 1
 24 
 25 /* 設定檔案為非阻塞 */
 26 int set_nonblock(int fd)
 27 {
 28     int old_flags = fcntl(fd, F_GETFL);
 29     fcntl(fd, F_SETFL, old_flags | O_NONBLOCK);
 30     return old_flags;
 31 }
 32 
 33 /* 註冊檔案描述符到epoll,並設定其事件為EPOLLIN(可讀事件) */
 34 void addfd_to_epoll(int epoll_fd, int fd, int epoll_type, int block_type)
 35 {
 36     struct epoll_event ep_event;
 37     ep_event.data.fd = fd;
 38     ep_event.events = EPOLLIN;
 39 
 40     /* 如果是ET模式,設定EPOLLET */
 41     if (epoll_type == EPOLL_ET)
 42         ep_event.events |= EPOLLET;
 43 
 44     /* 設定是否阻塞 */
 45     if (block_type == FD_NONBLOCK)
 46         set_nonblock(fd);
 47 
 48     epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ep_event);
 49 }
 50 
 51 /* LT處理流程 */
 52 void epoll_lt(int sockfd)
 53 {
 54     char buffer[MAX_BUFFER_SIZE];
 55     int ret;
 56 
 57     memset(buffer, 0, MAX_BUFFER_SIZE);
 58     printf("開始recv()...\n");
 59     ret = recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);
 60     printf("ret = %d\n", ret);
 61     if (ret > 0)
 62         printf("收到訊息:%s, 共%d個位元組\n", buffer, ret);
 63     else
 64     {
 65         if (ret == 0)
 66             printf("客戶端主動關閉!!!\n");
 67         close(sockfd);
 68     }
 69 
 70     printf("LT處理結束!!!\n");
 71 }
 72 
 73 /* 帶迴圈的ET處理流程 */
 74 void epoll_et_loop(int sockfd)
 75 {
 76     char buffer[MAX_BUFFER_SIZE];
 77     int ret;
 78 
 79     printf("帶迴圈的ET讀取資料開始...\n");
 80     while (1)
 81     {
 82         memset(buffer, 0, MAX_BUFFER_SIZE);
 83         ret = recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);
 84         if (ret == -1)
 85         {
 86             if (errno == EAGAIN || errno == EWOULDBLOCK)
 87             {
 88                 printf("迴圈讀完所有資料!!!\n");
 89                 break;
 90             }
 91             close(sockfd);
 92             break;
 93         }
 94         else if (ret == 0)
 95         {
 96             printf("客戶端主動關閉請求!!!\n");
 97             close(sockfd);
 98             break;
 99         }
100         else
101             printf("收到訊息:%s, 共%d個位元組\n", buffer, ret);
102     }
103     printf("帶迴圈的ET處理結束!!!\n");
104 }
105 
106 
107 /* 不帶迴圈的ET處理流程,比epoll_et_loop少了一個while迴圈 */
108 void epoll_et_nonloop(int sockfd)
109 {
110     char buffer[MAX_BUFFER_SIZE];
111     int ret;
112 
113     printf("不帶迴圈的ET模式開始讀取資料...\n");
114     memset(buffer, 0, MAX_BUFFER_SIZE);
115     ret = recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);
116     if (ret > 0)
117     {
118         printf("收到訊息:%s, 共%d個位元組\n", buffer, ret);
119     }
120     else
121     {
122         if (ret == 0)
123             printf("客戶端主動關閉連線!!!\n");
124         close(sockfd);
125     }
126 
127     printf("不帶迴圈的ET模式處理結束!!!\n");
128 }
129 
130 /* 處理epoll的返回結果 */
131 void epoll_process(int epollfd, struct epoll_event *events, int number, int sockfd, int epoll_type, int block_type)
132 {
133     struct sockaddr_in client_addr;
134     socklen_t client_addrlen;
135     int newfd, connfd;
136     int i;
137 
138     for (i = 0; i < number; i++)
139     {
140         newfd = events[i].data.fd;
141         if (newfd == sockfd)
142         {
143             printf("=================================新一輪accept()===================================\n");
144             printf("accept()開始...\n");
145 
146             /* 休眠3秒,模擬一個繁忙的伺服器,不能立即處理accept連線 */
147             printf("開始休眠3秒...\n");
148             sleep(3);
149             printf("休眠3秒結束!!!\n");
150 
151             client_addrlen = sizeof(client_addr);
152             connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addrlen);
153             printf("connfd = %d\n", connfd);
154 
155             /* 註冊已連結的socket到epoll,並設定是LT還是ET,是阻塞還是非阻塞 */
156             addfd_to_epoll(epollfd, connfd, epoll_type, block_type);
157             printf("accept()結束!!!\n");
158         }
159         else if (events[i].events & EPOLLIN)
160         {
161             /* 可讀事件處理流程 */
162 
163             if (epoll_type == EPOLL_LT)    
164             {
165                 printf("============================>水平觸發開始...\n");
166                 epoll_lt(newfd);
167             }
168             else if (epoll_type == EPOLL_ET)
169             {
170                 printf("============================>邊緣觸發開始...\n");
171 
172                 /* 帶迴圈的ET模式 */
173                 epoll_et_loop(newfd);
174 
175                 /* 不帶迴圈的ET模式 */
176                 //epoll_et_nonloop(newfd);
177             }
178         }
179         else
180             printf("其他事件發生...\n");
181     }
182 }
183 
184 /* 出錯處理 */
185 void err_exit(char *msg)
186 {
187     perror(msg);
188     exit(1);
189 }
190 
191 /* 建立socket */
192 int create_socket(const char *ip, const int port_number)
193 {
194     struct sockaddr_in server_addr;
195     int sockfd, reuse = 1;
196 
197     memset(&server_addr, 0, sizeof(server_addr));
198     server_addr.sin_family = AF_INET;
199     server_addr.sin_port = htons(port_number);
200 
201     if (inet_pton(PF_INET, ip, &server_addr.sin_addr) == -1)
202         err_exit("inet_pton() error");
203 
204     if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1)
205         err_exit("socket() error");
206 
207     /* 設定複用socket地址 */
208     if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)
209         err_exit("setsockopt() error");
210 
211     if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
212         err_exit("bind() error");
213 
214     if (listen(sockfd, 5) == -1)
215         err_exit("listen() error");
216 
217     return sockfd;
218 }
219 
220 /* main函式 */
221 int main(int argc, const char *argv[])
222 {
223     if (argc < 3)
224     {
225         fprintf(stderr, "usage:%s ip_address port_number\n", argv[0]);
226         exit(1);
227     }
228 
229     int sockfd, epollfd, number;
230 
231     sockfd = create_socket(argv[1], atoi(argv[2]));
232     struct epoll_event events[MAX_EPOLL_EVENTS];
233 
234     /* linux核心2.6.27版的新函式,和epoll_create(int size)一樣的功能,並去掉了無用的size引數 */
235     if ((epollfd = epoll_create1(0)) == -1)
236         err_exit("epoll_create1() error");
237 
238     /* 以下設定是針對監聽的sockfd,當epoll_wait返回時,必定有事件發生,
239      * 所以這裡我們忽略罕見的情況外設定阻塞IO沒意義,我們設定為非阻塞IO */
240 
241     /* sockfd:非阻塞的LT模式 */
242     addfd_to_epoll(epollfd, sockfd, EPOLL_LT, FD_NONBLOCK);
243 
244     /* sockfd:非阻塞的ET模式 */
245     //addfd_to_epoll(epollfd, sockfd, EPOLL_ET, FD_NONBLOCK);
246 
247    
248     while (1)
249     {
250         number = epoll_wait(epollfd, events, MAX_EPOLL_EVENTS, -1);
251         if (number == -1)
252             err_exit("epoll_wait() error");
253         else
254         {
255             /* 以下的LT,ET,以及是否阻塞都是是針對accept()函式返回的檔案描述符,即函式裡面的connfd */
256 
257             /* connfd:阻塞的LT模式 */
258             epoll_process(epollfd, events, number, sockfd, EPOLL_LT, FD_BLOCK);
259 
260             /* connfd:非阻塞的LT模式 */
261             //epoll_process(epollfd, events, number, sockfd, EPOLL_LT, FD_NONBLOCK);
262 
263             /* connfd:阻塞的ET模式 */
264             //epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_BLOCK);
265 
266             /* connfd:非阻塞的ET模式 */
267             //epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_NONBLOCK);
268         }
269     }
270 
271     close(sockfd);
272     return 0;
273 }

1.驗證水平觸發的非阻塞sockfd,關鍵程式碼在242行。編譯執行

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

程式碼裡面休眠了3秒,模擬繁忙伺服器不能很快處理accept()請求。這裡,我們開另一個終端快速用5個連線連到伺服器:

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

我們再看看伺服器的反映,可以看到5個終端連線都處理完成了,返回的新connfd依次為5,6,7,8,9:

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

上面測試完畢後,我們批量kill掉那5個客戶端,方便後面的測試:

1 $:for i in {1..5};do kill %$i;done

2.邊緣觸發的非阻塞sockfd,我們註釋掉242行的程式碼,放開245行的程式碼。編譯執行後,用同樣的方法,快速建立5個客戶端連線,或者測試5個後再測試10個。再看伺服器的反映,5個客戶端只處理了2個。說明高併發時,會出現客戶端連線不上的問題:

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

3.水平觸發的阻塞connfd,我們先把sockfd改回到水平觸發,註釋245行的程式碼,放開242行。重點程式碼在258行。
編譯執行後,用一個客戶端連線,併傳送1-9這幾個數:

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

再看伺服器的反映,可以看到水平觸發觸發了2次。因為我們程式碼裡面設定的緩衝區是5位元組,處理程式碼一次接收不完,水平觸發一直觸發,直到資料全部讀取完畢:

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

4.水平觸發的非阻塞connfd。註釋263行的程式碼,放開261行的程式碼。同上面那樣測試,我們可以看到伺服器反饋的訊息跟上面測試一樣。這裡我就不再截圖。
5.邊緣觸發的阻塞connfd,註釋其他測試程式碼,放開264行的程式碼。先測試不帶迴圈的ET模式(即不迴圈讀取資料,跟水平觸發讀取一樣),註釋173行的程式碼,放開176行的程式碼。
編譯執行後,開啟一個客戶端連線,併傳送1-9這幾個數字,再看看伺服器的反映,可以看到邊緣觸發只觸發了一次,只讀取了5個位元組:

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

我們繼續在剛才的客戶端傳送一個字元a,告訴epoll_wait(),有新的可讀事件發生:

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

再看看伺服器,伺服器又觸發了一次新的邊緣觸發,並繼續讀取上次沒讀完的6789加一個回車符:

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

這個時候,如果繼續在剛剛的客戶端再傳送一個a,客戶端這個時候就會讀取上次沒讀完的a加上次的回車符,2個位元組,還剩3個位元組的緩衝區就可以讀取本次的a加本次的回車符共4個位元組:

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

我們可以看到,阻塞的邊緣觸發,如果不一次性讀取一個事件上的資料,會干擾下一個事件!!!
接下來,我們就一次性讀取資料,即帶迴圈的ET模式。注意:我們這裡測試的還是邊緣觸發的阻塞connfd,只是換個讀取資料的方式。
註釋176行程式碼,放開173的程式碼。編譯執行,依然用一個客戶端連線,傳送1-9。看看伺服器,可以看到資料全部讀取完畢:

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

細心的朋友肯定發現了問題,程式沒有輸出”帶迴圈的ET處理結束”,是因為程式一直卡在了83行的recv()函式上,因為是阻塞IO,如果沒資料可讀,它會一直等在那裡,直到有資料可讀。如果這個時候,用另一個客戶端去連線,伺服器不能受理這個新的客戶端!!!
6.邊緣觸發的非阻塞connfd,不帶迴圈的ET測試同上面一樣,資料不會讀取完。這裡我們就只需要測試帶迴圈的ET處理,即正規的邊緣觸發用法。註釋其他測試程式碼,放開267行程式碼。編譯執行,用一個客戶端連線,併傳送1-9。再觀測伺服器的反映,可以看到資料全部讀取完畢,處理函式也退出了,因為非阻塞IO如果沒有資料可讀時,會立即返回,並設定error,這裡我們根據EAGAIN和EWOULDBLOCK來判斷資料全部讀取完畢了,可以退出迴圈了:

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

這個時候,我們用另一個客戶端去連線,伺服器依然可以正常接收請求:

「linux」例項淺析epoll的LT和ET模式,ET模式為何要使用非阻塞IO

1.對於監聽的sockfd,最好使用水平觸發模式,邊緣觸發模式會導致高併發情況下,有的客戶端會連線不上。如果非要使用邊緣觸發,網上有的方案是用while來迴圈accept()。
2.對於讀寫的connfd,水平觸發模式下,阻塞和非阻塞效果都一樣,不過為了防止特殊情況,還是建議設定非阻塞。
3.對於讀寫的connfd,邊緣觸發模式下,必須使用非阻塞IO,並要一次性全部讀寫完資料。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章