https://blog.csdn.net/men_wen/article/details/53456474
Linux網絡編程—I/O復用模型之epoll
1. epoll模型簡介
epoll是Linux多路服用IO接口select/poll的加強版,e對應的英文單詞就是enhancement,中文翻譯為增強,加強,提高,充實的意思。所以epoll模型會顯著提高程序在大量并發連接中只有少量活躍的情況下的系統CPU利用率。
- epoll把用戶關心的文件描述符上的時間放在內核的一個事件表中,無需像select和poll那樣每次調用都重復傳入文件描述符集。
- epoll在獲取事件的時候,無需遍歷整個被監聽的文件描述符集合,而是遍歷那些被內核IO事件異步喚醒而加入ready隊列的描述符集合。
所以,epoll是Linux大規模高并發網絡程序的首選模型。
2.epoll模型的API
epoll使用一組函數來完成任務
2.1 函數epoll_create
創建一個epoll句柄,句柄的英文是handle,相通的意思是把手,把柄。
#include <sys/epoll.h>int epoll_create(int size);
//返回值:若成功,返回一個非負的文件描述符,若出錯,返回-1。
- 該函數返回一個文件描述符,用來唯一標示內核中這個事件表,sizeof參數提示內核要監聽的文件描述符個數,這與內存大小有關。
- 返回的文件描述符將是其他所有epoll系統調用的第一個參數,以指定要訪問的內核時間表,所以用該返回的文件描述符相當與其他epoll調用的把手、把柄一樣。
查看進程能夠打開的最大數目的文件描述符
? ~ cat /proc/sys/fs/file-max
1215126
//該值與內存大小有關
修改最大文件描述符限制
? ~ sudo vim /etc/security/limits.conf
//重啟生效
2.2 函數epoll_ctl
該函數用來操作epoll的內核事件表
#include <sys/epoll.h>int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//返回值:若成功,返回0,若出錯返回-1。
- epfd就是函數epoll_create創建的句柄。
- op是指定操作類型,有一下三種?
- EPOLL_CTL_ADD,向epfd注冊fd的上的event
- EPOLL_CTL_MOD,修改fd已注冊的event
- EPOLL_CTL_DEL,從epfd上刪除fd的event?
- fd是操作的文件描述符
- event指定內核要監聽事件,它是struct epoll_event結構類型的指針。epoll_event定義如下:
struct epoll_event {uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
};
vents成員描述事件類型,將以下宏定義通過位或方式組合
- EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉)
- POLLOUT:表示對應的文件描述符可以寫
- EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來)
- EPOLLERR:表示對應的文件描述符發生錯誤
- EPOLLHUP:表示對應的文件描述符被掛斷;
- EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水平觸發(Level Triggered)來說的
- EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
data用于存儲用戶數據,是epoll_data_t結構類型,該結構定義如下:
typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;
- epoll_data_t是一個聯合體,fd指定事件所從屬的目標文件描述符。ptr可以用來指定fd相關的用戶數據,但兩者不能同時使用。
2.3 函數epoll_wait
函數epoll_wait用來等待所監聽文件描述符上有事件發生
#include <sys/epoll.h>int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
//返回值:若成功,返回就緒的文件描述符個數,若出錯,返回-1,時間超時返回0
- epfd就是函數epoll_create創建的句柄
- timeout是超時事件,-1為阻塞,0為立即返回,非阻塞,大于0是指定的微妙
- events是一個 傳入傳出參數,它是epoll_event結構的指針,用來從內核得到事件的集合
- maxevents告知內核events的大小,但不能大于epoll_create()時創建的size
3. LT和ET模式
- LT(Level Triggered,電平觸發):LT模式是epoll默認的工作模式,也是select和poll的工作模式,在LT模式下,epoll相當于一個效率較高的poll。?
- 采用LT模式的文件描述符,當epoll_wait檢測到其上有事件發生并將此事件通知應用程序后,應用程序可以不立即處理此事件,當下一次調用epoll_wait是,epoll_wait還會將此事件通告應用程序。
- ET(Edge Triggered,邊沿觸發):當調用epoll_ctl,向參數event注冊EPOLLET事件時,epoll將以ET模式來操作該文件描述符,ET模式是epoll的高效工作模式.?
- 對于采用ET模式的文件描述符,當epoll_wait檢測到其上有事件發生并將此通知應用程序后,應用程序必須立即處理該事件,因為后續的epoll_wait調用將不在向應用程序通知這一事件。ET模式降低了同意epoll事件被觸發的次數,效率比LT模式高。
4. LT和ET的服務端和客戶端代碼
4.1 服務器端
#include <sys/epoll.h>
#include <fcntl.h>
#include "wrap.h"#define MAX_EVENT_NUM 1024
#define BUFFER_SIZE 10
#define true 1
#define false 0int setnonblocking(int fd)
{int old_opt = fcntl(fd, F_GETFD);int new_opt = old_opt | O_NONBLOCK;fcntl(fd, F_SETFD, new_opt);return old_opt;
}//將文件描述符設置為非阻塞的void addfd(int epollfd, int fd, int enable_et)
{struct epoll_event event;event.data.fd = fd;event.events = EPOLLIN;if(enable_et){event.events |= EPOLLET;}epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
// setnonblocking(fd);
}//將文件描述符fd的EPOLLIN注冊到epollfd指示的epoll內核事件表中,enable_et表示是否對fd啟用ET模式void lt(struct epoll_event *events, int num, int epollfd, int listenfd)
{char buf[BUFFER_SIZE];for(int i = 0; i < num; i++){int sockfd = events[i].data.fd;if(sockfd == listenfd){struct sockaddr_in clientaddr;socklen_t clilen = sizeof(clientaddr);int connfd = Accept(listenfd, (struct sockaddr *)&clientaddr, &clilen);addfd(epollfd, connfd, false);//對connfd使用默認的lt模式}else if(events[i].events & EPOLLIN){//只要socket讀緩存中還有未讀的數據,這段代碼就會觸發printf("event trigger once\n");memset(buf, '\0', BUFFER_SIZE);int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);if(ret <= 0){Close(sockfd);continue;}printf("get %d bytes of content:%s\n", ret, buf);}else{printf("something else happened\n");}}
}void et(struct epoll_event *event, int num, int epollfd, int listenfd)
{char buf[BUFFER_SIZE];for(int i = 0; i < num; i++){int sockfd = event[i].data.fd;if(sockfd == listenfd){struct sockaddr_in clientaddr;int clilen = sizeof(clientaddr);int connfd = Accept(listenfd, (struct sockaddr *)&clientaddr, &clilen);addfd(epollfd, connfd, true);//多connfd開啟ET模式}else if(event[i].events & EPOLLIN){printf("event trigger once\n");while(1){//這段代碼不會重復觸發,所以要循環讀取數據memset(buf, '\0', BUFFER_SIZE);int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);if(ret < 0){if((errno == EAGAIN) || (errno == EWOULDBLOCK)){printf("read later\n");break;}Close(sockfd);break;}else if(ret == 0){Close(sockfd);}else{printf("get %d bytes of content:%s\n", ret, buf);}}}else{printf("something else happened \n");}}
}int start_ser(char *ipaddr, char *port)
{int sock = Socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in serveraddr;bzero(&serveraddr, sizeof(serveraddr));serveraddr.sin_family = AF_INET;serveraddr.sin_port = htons(atoi(port));inet_pton(AF_INET, ipaddr, &serveraddr.sin_addr);Bind(sock, (struct sockaddr *)&serveraddr, sizeof(serveraddr));Listen(sock, 128);return sock;
}int main(int argc, char *argv[])
{int listenfd = start_ser(argv[1], argv[2]);struct epoll_event events[MAX_EVENT_NUM];int epollfd = epoll_create(5);if(epollfd < 0){perr_exit("epoll_create err");}addfd(epollfd, listenfd, true);while(1){int ret = epoll_wait(epollfd, events, MAX_EVENT_NUM, -1);if(ret < 0){printf("epoll failure\n");break;}lt(events, ret, epollfd, listenfd);//lt模式//et(events, ret, epollfd, listenfd);//et模式}Close(listenfd);return 0;
}
//warp.h文件是將socket,bind,listen等函數封裝為第一個字母大寫的頭文件
4.2 客戶端
#include "wrap.h" int main(int argc, char *argv[])
{int connfd;struct sockaddr_in serveraddr;char buf[1024];connfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&serveraddr, sizeof(serveraddr));serveraddr.sin_family = AF_INET;serveraddr.sin_port = htons(atoi(argv[2]));inet_pton(AF_INET, argv[1], &serveraddr.sin_addr);Connect(connfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));while(fgets(buf, 1024, stdin) != NULL){Write(connfd, buf, strlen(buf));}Close(connfd);return 0;
}
4.3 兩種模式結果對比
?
當發送超過緩沖區大小的數據量,LT會多次調用epoll_wait函數接受數據,則打印了多次“event level once”,而ET則是循環讀取數據知道讀完,打印了一次“event trigger once”。