epoll哪些触发模式_网络编程:epoll
前言
前面講了IO多路復用的API,select和poll的缺點是性能不夠,客戶端連接越多性能下降越明顯,epoll的出現解決了這個問題,引用The Linux Programming Interface的一個統計對比如下:
fd數量 poll CPU時間(秒) select CPU時間(秒) epoll CPU時間(秒) --------------------------------------------------------------------- 10 0.61 0.73 0.41 100 2.9 3.0 0.42 1000 35 35 0.53 10000 990 930 0.66 ---------------------------------------------------------------------可以看出fd達到100個以后,select/poll就非常慢了,而epoll即使達到10000個也表現得非常好,因為:
- 每次調用select/poll,內核必須檢查所有傳進來的描述符;而對于epoll,每次調用epoll_ctl,內核會把相關信息與底層的文件描述關聯起來,當IO事件就緒時,內核把信息加到epoll的就緒列表里。隨后調用epoll_wait,內核只需把就緒列表中的信息提取出來返回即可。
- 每次調用select/poll,都要把待監控的所有文件描述符傳給內核,函數返回時,內核要把描述符返回并標識哪些就緒,得到結果后還要逐個判斷所有描述符,才能確定哪些有事件;epoll在調用epoll_ctl時就已經維護著監控的列表,epoll_wait不需要傳入任何信息,并且返回的結果只包含就緒的描述符,這樣就不用去判斷所有描述符。
從概念上理解epoll是這樣的,把要監控的fd的IO事件注冊給epoll(調用epoll_ctl),然后調用epoll的API等待事件到達(調用epoll_wait),內核可能對每個fd維護著一個讀和寫的緩沖區,那么:
- 如果我監控讀事件,并且讀緩沖區有數據了,epoll_wait就會返回,此時我可以調用read讀數據。
- 如果我監控寫事件,并且寫緩存區未滿,epoll_wait也會返回,此時我可以調用write寫數據。
- 如果fd發生了一些錯誤,epoll_wait也會返回,此時我根據返回的標志位,就可以知道。
- 如果我監控讀事件 并且有客戶端連接進來,epoll_wait就會返回,此時我可以調用accept接受客戶端。
epoll的API介紹
- int epoll_create(int size);
創建一個epoll實例,返回代表實例的文件描述符(fd),size自Linux 2.6.8以后忽略,但必須大于0. - int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll控制接口,epfd就是epoll的文件描述符,fd是要操作的文件描述符,op有如下幾種:- EPOLL_CTL_ADD 注冊fd的事件,事件類型在event指定。
- EPOLL_CTL_MOD 修改已注冊的fd事件。
- EPOLL_CTL_DEL 刪除fd的事件。
epoll_event有一個events成員,指定要注冊的事件類型,比較重要的幾個:
- EPOLLIN fd可讀事件
- EPOLLOUT fd可寫事件
- EPOLLERR fd發生錯誤,這個事件總是會被監控,不必手動增加
- EPOLLHUP fd被掛起時,這個事件總是會被監控,不必手動增加,這通常發生在socket異常關閉時,此時read返回0,然后正常的清理socket資源。
epoll_event還有一個epoll_data_t成員,由外部設置自定義數據,以方便后續處理。
- int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件發生,如果沒有事件發生,線程會被掛起,maxevents指定最大事件數,events外部傳入的事件數組,長度應當等于maxevents,當事件發生時,epoll會把事件信息填到這里, timeout指定等待的最大時間, 0表示馬上返回,-1表示無限等待。
epoll_wait返回等待到的事件數,返回時,遍歷events對fd進行處理。 當epoll不再使用時,應該調用close關閉epollfd。
水平觸發和邊緣觸發
epoll觸發事件有兩種模式,默認的叫水平觸發(LT),另一種叫邊緣觸發(ET):
- LT模式:只要fd的讀緩沖區不空,或寫緩沖區不滿,epoll_wait就會一直觸發事件(也就是返回)。
- ET模式:當被監控的fd狀態變化時(從未就緒變成就緒狀態),事件觸發一次。此后內核不再通知,除非有新的事件到來。
// 多謝 @黃蔚 的指正,原來ET模式的描述有誤,仔細閱讀過man文檔后已修正過來。
LT處理起來比ET要簡單得多,讀事件觸發,只需要read一次,如果數據沒讀完,下次epoll_wait還會返回,寫也是一樣的;ET模式就要求事件觸發時,一直讀一直寫直到明確知道已經讀寫完畢(返回EAGIN或EWOULDBLOCK的錯誤碼)。
水平觸發的服務器程序大概是這樣的流程:
- accept一個新連接,將這個新連接的fd加到epoll事件中,監聽EPOLLIN事件。
- EPOLLIN事件到達時,read該fd中的數據。
- 如果要向該fd寫事件,向epoll增加EPOLLOUT事件。
- EPOLLOUT事件到達,向fd write數據,如果數據太大無法一次寫出,那么先保留EPOLLOUT事件,下次事件到達繼續寫;如果寫出完畢,從epoll刪除EPOLLOUT事件。
一個實用的echo程序:
這一次我們要使用epoll和非阻塞socket來寫一個真正實用的echo服務器,調用fcntl函數,設置O_NONBLOCK標志位,即可讓socket的文件描述符變成非阻塞模式。非阻塞模式處理起來比阻塞模式要復雜一些:
- read, write, accept這些函數不會阻塞,要么成功,要么返回-1失敗,errno記錄了失敗的原因,有幾個錯誤碼要關注:
- EAGAIN 或 EWOULDBLOCK 只有非阻塞的fd才會發生,表示沒數據可讀,或沒空間可寫,或沒有客戶端可接受,下次再來吧。這兩個值可能相同也可能不同,最好一起判斷。
- EINTR 表示被信號中斷,這種情況可以再一次嘗試調用。
- 其他錯誤表示真的出錯了。
- 向一個fd寫數據比較麻煩,我們沒法保證一次性把所有數據都寫完,所以需要先保存在緩沖里,然后向epoll增加寫事件,事件觸發時再向fd寫數據。等數據都寫完了,再把事件從epoll移除。這個程序把寫數據保存在鏈表中。
我們把監聽fd保留為阻塞模式,因為epoll_wait返回,可以確定一定有客戶端連接進來,所以accept一般可以成功,并不用擔心會阻塞??蛻舳诉B接使用的是非阻塞模式,確保讀寫未完成時不會阻塞。
下面是這個程序的代碼,關鍵地方加了一些注釋,仔細看代碼比看文字描述更有用:)
#include "socket_lib h" #include <unistd.h> #include <assert.h> #include <errno.h> #include <fcntl.h> #include <sys/epoll.h>#define MAX_CLIENT 10000 #define MIN_RSIZE 124 #define BACKLOG 128 #define EVENT_NUM 64// 緩存結點 struct twbuffer {struct twbuffer *next; // 下一個緩存void *buffer; // 緩存char *ptr; // 當前未發送的緩存,buffer != ptr表示只發送了一部分int size; // 當前未發送的緩存大小 };// 緩存列表 struct twblist {struct twbuffer *head;struct twbuffer *tail; };// 客戶端連接信息 struct tclient {int fd; // 客戶端fdint rsize; // 當前讀的緩存區大小int wbsize; // 還未寫完的緩存大小struct twblist wblist; // 寫緩存鏈表 };// 服務器信息 struct tserver {int listenfd; // 監聽fdint epollfd; // epollfdstruct tclient clients[MAX_CLIENT]; // 客戶端結構數組 };// epoll增加讀事件 void epoll_add(int efd, int fd, void *ud) {struct epoll_event ev;ev.events = EPOLLIN;ev.data.ptr = ud;epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ev); }// epoll修改寫事件 void epoll_write(int efd, int fd, void *ud, int enabled) {struct epoll_event ev;ev.events = EPOLLIN | (enabled ? EPOLLOUT : 0);ev.data.ptr = ud;epoll_ctl(efd, EPOLL_CTL_MOD, fd, &ev); }// epoll刪除fd void epoll_del(int efd, int fd) {epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL); }// 設置socket為非阻塞 void set_nonblocking(int fd) {int flag = fcntl(fd, F_GETFL, 0);if (flag >= 0) {fcntl(fd, F_SETFL, flag | O_NONBLOCK);} }// 增加寫緩存 void add_wbuffer(struct twblist *list, void *buffer, int sz) {struct twbuffer *wb = malloc(sizeof(*wb));wb->buffer = buffer;wb->ptr = buffer;wb->size = sz;wb->next = NULL;if (!list->head) {list->head = list->tail = wb;} else {list->tail->next = wb;list->tail = wb;} }// 釋放寫緩存 void free_wblist(struct twblist *list) {struct twbuffer *wb = list->head;while (wb) {struct twbuffer *tmp = wb;wb = wb->next;free(tmp);}list->head = NULL;list->tail = NULL; }// 創建客戶端信息 struct tclient* create_client(struct tserver *server, int fd) {int i;struct tclient *client = NULL;for (i = 0; i < MAX_CLIENT; ++i) {if (server->clients[i].fd < 0) {client = &server->clients[i];break;}}if (client) {client->fd = fd;client->rsize = MIN_RSIZE;set_nonblocking(fd); // 設為非阻塞模式epoll_add(server->epollfd, fd, client); // 增加讀事件return client;} else {fprintf(stderr, "too many client: %dn", fd);close(fd);return NULL;} }// 關閉客戶端 void close_client(struct tserver *server, struct tclient *client) {assert(client->fd >= 0);epoll_del(server->epollfd, client->fd);if (close(client->fd) < 0) perror("close: ");client->fd = -1;client->wbsize = 0;free_wblist(&client->wblist); }// 初始化服務信息 struct tserver* create_server(const char *host, const char *port) {struct tserver *server = malloc(sizeof(*server));memset(server, 0, sizeof(*server));for (int i = 0; i < MAX_CLIENT; ++i) {server->clients[i].fd = -1;}server->epollfd = epoll_create(MAX_CLIENT);server->listenfd = tcpListen(host, port, BACKLOG);epoll_add(server->epollfd, server->listenfd, NULL);return server; }// 釋放服務器 void release_server(struct tserver *server) {for (int i = 0; i < MAX_CLIENT; ++i) {struct tclient *client = &server->clients[i];if (client->fd >= 0) {close_client(server, client);}}epoll_del(server->epollfd, server->listenfd);close(server->listenfd);close(server->epollfd);free(server); }// 處理接受 void handle_accept(struct tserver *server) {struct sockaddr_storage claddr;socklen_t addrlen = sizeof(struct sockaddr_storage);for (;;) {int cfd = accept(server->listenfd, (struct sockaddr*)&claddr, &addrlen);if (cfd < 0) {int no = errno;if (no == EINTR)continue;perror("accept: ");exit(1); // 出錯}char host[NI_MAXHOST];char service[NI_MAXSERV];if (getnameinfo((struct sockaddr *)&claddr, addrlen, host, NI_MAXHOST, service, NI_MAXSERV, 0) == 0)printf("client connect: fd=%d, (%s:%s)n", cfd, host, service);elseprintf("client connect: fd=%d, (?UNKNOWN?)n", cfd);create_client(server, cfd);break;} }// 處理讀 void handle_read(struct tserver *server, struct tclient *client) {int sz = client->rsize;char *buf = malloc(sz);ssize_t n = read(client->fd, buf, sz);if (n < 0) { // errorfree(buf);int no = errno;if (no != EINTR && no != EAGAIN && no != EWOULDBLOCK) {perror("read: ");close_client(server, client);}return;}if (n == 0) { // client closefree(buf);printf("client close: %dn", client->fd);close_client(server, client);return;}// 確定下一次讀的大小if (n == sz)client->rsize >>= 1;else if (sz > MIN_RSIZE && n *2 < sz)client->rsize <<= 1;// 加入寫緩存add_wbuffer(&client->wblist, buf, n);// 增加寫事件epoll_write(server->epollfd, client->fd, client, 1); }// 處理寫 void handle_write(struct tserver *server, struct tclient *client) {struct twblist *list = &client->wblist;while (list->head) {struct twbuffer *wb = list->head;for (;;) {ssize_t sz = write(client->fd, wb->ptr, wb->size);if (sz < 0) {int no = errno;if (no == EINTR) // 信號中斷,繼續continue;else if (no == EAGAIN || no == EWOULDBLOCK) // 內核緩沖滿了,下次再來return;else { // 其他錯誤 perror("write: ");close_client(server, client);return;}}client->wbsize -= sz;if (sz != wb->size) { // 未完全發送出去,下次再來wb->ptr += sz;wb->size -= sz;return;}break;}list->head = wb->next;free(wb);}list->tail = NULL;// 到這里寫全部完成,關閉寫事件epoll_write(server->epollfd, client->fd, client, 0); }// 先處理錯誤 void handle_error(struct tserver *server, struct tclient *client) {perror("client error: ");close_client(server, client); }int main() {signal(SIGPIPE, SIG_IGN);struct tserver *server = create_server("127.0.0.1", "3459");struct epoll_event events[EVENT_NUM];for (;;) {int nevent = epoll_wait(server->epollfd, events, EVENT_NUM, -1);if (nevent <= 0) {if (nevent < 0 && errno != EINTR) {perror("epoll_wait: ");return 1;}continue;}int i = 0;for (i = 0; i < nevent; ++i) {struct epoll_event ev = events[i];if (ev.data.ptr == NULL) { // accepthandle_accept(server);} else {if (ev.events & (EPOLLIN | EPOLLHUP)) { // readhandle_read(server, ev.data.ptr);}if (ev.events & EPOLLOUT) { // writehandle_write(server, ev.data.ptr);}if (ev.events & EPOLLERR) { // errorhandle_error(server, ev.data.ptr);}}}}release_server(server);return 0; } 與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的epoll哪些触发模式_网络编程:epoll的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: angularjs增删改查数据_Mong
- 下一篇: canvas js 绘图插件_Canva