實例淺析epoll的水平觸發和邊緣觸發

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

一.基本概念                                                         html

咱們通俗一點講:linux

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

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

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

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

二.幾種IO模型的觸發方式                          函數

 select(),poll()模型都是水平觸發模式,信號驅動IO是邊緣觸發模式,epoll()模型即支持水平觸發,也支持邊緣觸發,默認是水平觸發。高併發

這裏咱們要探討epoll()的水平觸發和邊緣觸發,以及阻塞IO和非阻塞IO對它們的影響!!!下面稱水平觸發爲LT,邊緣觸發爲ET。oop

對於監聽的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,並要一次性所有讀寫完數據。

相關文章
相關標籤/搜索