實例淺析epoll的水平觸發和邊緣觸發,以及邊緣觸發為什麼要使用非阻塞IO

来源:http://www.cnblogs.com/yuuyuu/archive/2016/01/05/5103744.html
-Advertisement-
Play Games

一.基本概念 我們通俗一點講:Level_triggered(水平觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程式去讀寫。如果這次沒有把數據一次性全部讀寫完(如讀寫緩衝區太小),那麼下次調用 epoll_w...


一.基本概念                                                         

我們通俗一點講:

Level_triggered(水平觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程式去讀寫。如果這次沒有把數據一次性全部讀寫完(如讀寫緩衝區太小),那麼下次調用 epoll_wait()時,它還會通知你在上沒讀寫完的文件描述符上繼續讀寫,當然如果你一直不去讀寫,它會一直通知你!!!如果系統中有大量你不需要讀寫的就緒文件描述符,而它們每次都會返回,這樣會大大降低處理程式檢索自己關心的就緒文件描述符的效率!!!

Edge_triggered(邊緣觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程式去讀寫。如果這次沒有把數據全部讀寫完(如讀寫緩衝區太小),那麼下次調用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現第二次可讀寫事件才會通知你!!!這種模式比水平觸發效率高,系統不會充斥大量你不關心的就緒文件描述符!!!

阻塞IO:當你去讀一個阻塞的文件描述符時,如果在該文件描述符上沒有數據可讀,那麼它會一直阻塞(通俗一點就是一直卡在調用函數那裡),直到有數據可讀。當你去寫一個阻塞的文件描述符時,如果在該文件描述符上沒有空間(通常是緩衝區)可寫,那麼它會一直阻塞,直到有空間可寫。以上的讀和寫我們統一指在某個文件描述符進行的操作,不單單指真正的讀數據,寫數據,還包括接收連接accept(),發起連接connect()等操作...

非阻塞IO:當你去讀寫一個非阻塞的文件描述符時,不管可不可以讀寫,它都會立即返回,返回成功說明讀寫操作完成了,返回失敗會設置相應errno狀態碼,根據這個errno可以進一步執行其他處理。它不會像阻塞IO那樣,卡在那裡不動!!!

二.幾種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()模型沒有出現這種情況,我們就暫且忽略這種情況!!!

三.驗證代碼                                                         

文件名:epoll_lt_et.c

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

 

四.驗證                                                                

1.驗證水平觸發的非阻塞sockfd,關鍵代碼在247行。編譯運行

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

 

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

 

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

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

 

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

 

 

3.水平觸發的阻塞connfd,我們先把sockfd改回到水平觸發,註釋250行的代碼,放開247行。重點代碼在263行。

編譯運行後,用一個客戶端連接,併發送1-9這幾個數:

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

 

4.水平觸發的非阻塞connfd。註釋263行的代碼,放開266行的代碼。同上面那樣測試,我們可以看到伺服器反饋的消息跟上面測試一樣。這裡我就不再截圖。

5.邊緣觸發的阻塞connfd,註釋其他測試代碼,放開269行的代碼。先測試不帶迴圈的ET模式(即不迴圈讀取數據,跟水平觸發讀取一樣),註釋178行的代碼,放開181行的代碼。

編譯運行後,開啟一個客戶端連接,併發送1-9這幾個數字,再看看伺服器的反映,可以看到邊緣觸發只觸發了一次,只讀取了5個位元組:

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

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

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

我們可以看到,阻塞的邊緣觸發,如果不一次性讀取一個事件上的數據,會幹擾下一個事件!!!

 

接下來,我們就一次性讀取數據,即帶迴圈的ET模式。註意:我們這裡測試的還是邊緣觸發的阻塞connfd,只是換個讀取數據的方式。

註釋181行代碼,放開178的代碼。編譯運行,依然用一個客戶端連接,發送1-9。看看伺服器,可以看到數據全部讀取完畢:

 

細心的朋友肯定發現了問題,程式沒有輸出"帶迴圈的ET處理結束",是因為程式一直卡在了88行的recv()函數上,因為是阻塞IO,如果沒數據可讀,它會一直等在那裡,直到有數據可讀。如果這個時候,用另一個客戶端去連接,伺服器不能受理這個新的客戶端!!!

 

6.邊緣觸發的非阻塞connfd,不帶迴圈的ET測試同上面一樣,數據不會讀取完。這裡我們就只需要測試帶迴圈的ET處理,即正規的邊緣觸發用法。註釋其他測試代碼,放開272行代碼。編譯運行,用一個客戶端連接,併發送1-9。再觀測伺服器的反映,可以看到數據全部讀取完畢,處理函數也退出了,因為非阻塞IO如果沒有數據可讀時,會立即返回,並設置error,這裡我們根據EAGAIN和EWOULDBLOCK來判斷數據全部讀取完畢了,可以退出迴圈了:

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

 

五.總結                                                                  

1.對於監聽的sockfd,最好使用水平觸發模式,邊緣觸發模式會導致高併發情況下,有的客戶端會連接不上。如果非要使用邊緣觸發,網上有的方案是用while來迴圈accept()。

2.對於讀寫的connfd,水平觸發模式下,阻塞和非阻塞效果都一樣,不過為了防止特殊情況,還是建議設置非阻塞。

3.對於讀寫的connfd,邊緣觸發模式下,必須使用非阻塞IO,並要一次性全部讀寫完數據。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 要說,這也是一個很簡單的功能,沒必要開一篇博客這麼大動干戈。 對於一張知道全路徑的照片,如果其路徑包含尾碼名的話,要取得尾碼名,只需要一行代碼即可:1varext = System.IO.Path.GetExtension("C:\\soar.jpg");可是,如果這個文件的文件名不包含尾碼怎麼辦?...
  • 1.gzip, zcat[root@linux ~]# gzip [-cdt#] 檔名參數: -c :將壓縮的數據輸出到屏幕上,可透過數據流重導向來處理; -d :解壓縮的參數; -t :可以用來檢驗一個壓縮檔的一致性~看看檔案有無錯誤; -# :壓縮等級,-1 最快,但是壓縮比最差、-9 最慢,但...
  • 編寫者:龍詩科 郵箱:[email protected] gdb 是Linux 下預設的調試工具,當編譯一個程式文件時,如debug.c,要應用gdb調試,必須在終端命令前添加 cc -g或者gcc -g,則編譯debug.c可以用以下命令來得到gcc -g -o d.....
  • uptime命令回顯中的load average所表示的意思和w命令相似,都是表示過去的1分鐘、5分鐘和15分鐘內進程隊列中的平均進程數量。 這裡需要註意的是load average這個輸出值,這三個值的大小一般不能大於系統邏輯CPU的個數,例如,本輸出中系統有4個邏輯CPU,如果loa...
  • /*-----------------------------前奏-----------------------------------*/mcu型號s6e1c32c(48pin)、s6e1c32b(32pin)本次調試:uart0【與SWD 管腳復用】由於在啟動文件中 :跳轉到SystemInit...
  • 之前安裝Ubuntu時,給/boot單獨分了個區,200M...在內核不斷升級之後,這個區域越來越小,導致無法安裝系統更新...然後想到刪除不用的舊內核1. 查看系統現在使用的內核版本uname -a2. 查看系統中已有的內核dpkg --get-selections | grep linux3. ...
  • 編寫者:龍詩科郵箱:[email protected] 眾所周知,windows下的source insight是閱讀項目代碼的神器,其神奇之處在於可以根據當前滑鼠所指的函數名或者變數,來進行全局搜索該函數或變數的定義,而且還會顯示出哪些文件中同樣含有該函數定義,另外可....
  • 本系統是10月5日最新完整版本的Windows10安裝版鏡像,win10正式版,更新了重要補丁,提升應用載入速度,微軟和百度今天宣佈達成合作,百度成為win10Edge瀏覽器中國預設主頁和搜索引擎,系統增加了搜狗輸入法,安裝過程需要用戶手動創建個人賬戶等步驟,為保證系統的穩定性,win10為未激活版...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...