一.基本概念 html
咱們通俗一點講:linux
Level_triggered(水平觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。若是此次沒有把數據一次性所有讀寫完(如讀寫緩衝區過小),那麼下次調用 epoll_wait()時,它還會通知你在上沒讀寫完的文件描述符上繼續讀寫,固然若是你一直不去讀寫,它會一直通知你!!!若是系統中有大量你不須要讀寫的就緒文件描述符,而它們每次都會返回,這樣會大大下降處理程序檢索本身關心的就緒文件描述符的效率!!!緩存
Edge_triggered(邊緣觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll_wait()會通知處理程序去讀寫。若是此次沒有把數據所有讀寫完(如讀寫緩衝區過小),那麼下次調用epoll_wait()時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現第二次可讀寫事件纔會通知你!!!這種模式比水平觸發效率高,系統不會充斥大量你不關心的就緒文件描述符!!!服務器
阻塞IO:當你去讀一個阻塞的文件描述符時,若是在該文件描述符上沒有數據可讀,那麼它會一直阻塞(通俗一點就是一直卡在調用函數那裏),直到有數據可讀。當你去寫一個阻塞的文件描述符時,若是在該文件描述符上沒有空間(一般是緩衝區)可寫,那麼它會一直阻塞,直到有空間可寫。以上的讀和寫咱們統一指在某個文件描述符進行的操做,不僅僅指真正的讀數據,寫數據,還包括接收鏈接accept(),發起鏈接connect()等操做...併發
非阻塞IO:當你去讀寫一個非阻塞的文件描述符時,無論可不能夠讀寫,它都會當即返回,返回成功說明讀寫操做完成了,返回失敗會設置相應errno狀態碼,根據這個errno能夠進一步執行其餘處理。它不會像阻塞IO那樣,卡在那裏不動!!!socket
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 */
}; 高併發
二.幾種IO模型的觸發方式 oop
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,並要一次性所有讀寫完數據