Linux的I/O复用技术:epoll
epoll:
epoll是Linux特有的IO复用函数,被认为性能最好的一种方法,
它和select、poll在实现和使用上有很大差异:
1.使用一组函数来完成,而不是单个
2.把用户关心的文件描述符上的事件放在内核的一个事件表中,无须向select、poll那样每次调用都要重复传入文件描述符集或事件集,
但epoll需要用一个额外的文件描述符来表示内核中的这个事件表
epoll函数非常简单,有epoll_create,epoll_ctl,epoll_wait3个函数,先使epoll_create创建一个epoll的句柄,再通过epoll_ctl注册事件,然后epoll_wait检测事件的发生
优点:
具备了select所不具备的所有优点
1.没有fd数量的限制,它所支持的fd上限是最大可以打开文件的数目,具体数目可cat/proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大;
2.epoll_ctl每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝,
epoll保证了每个fd在整个过程中只会拷贝一次;
3.epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)
并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。
epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd;
适用场景:
当活动连接比较多的时候,epoll_wait的效率未必比select和poll高,因为此时回调函数被触发的过于频繁,因此epoll_wait适用于连接数量多,但活动连接较少的情况。
epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。这个文
件描述符使用如下epoll_create函数来创建:
#include<sys/epoll.h> int epoll_create(int size); /* size参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大。 该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); /* fd参数是要操作的文件描述符,op参数则指定操作类型。操作类型有如下3种: (1) EPOLL_CTL_ADD,往事件表中注册fd上的事件。 (2) EPOLL_CTL_MOD,修改fd上的注册事件. (3) EPOLL_CTL_DEL,删除fd上的注册事件。 event参数指定事件,它是 epoll_event结构指针类型。 epoll_event的定义如下: struct epoll_event { __uint32_t events;//epo11事件 epoll_data_t data; //用户数据 }; 其中 events成员描述事件类型。epoll支持的事件类型和poll基本相同。表示epoll事件类型的宏是在poll对应的宏前加上“E”,比如epoll的数据可读事件是 EPOLLIN。
但epoll有两个额外的事件类型————EPOLLET和 EPOLLONESHOT。 data成员用于存储用户数据,其类型 epoll_data_t的定义如下: typedef union epoll_data { void* ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; fd指定事件所从属的目标文件描述符; ptr成员可用来指定与fd相关的用户数据; 由于epoll_data_t是一个联合体,我们不能同时使用其ptr成员和fd成员。 */
int epoll_wait(int epfd, struct epoll_event* events, int maxevents ,int timeout); /* 该函数成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno。 maxevents参数指定最多监听多少个事件,它必须大于0。 epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数 events指向的数组中。 这个数组只用于输出 epoll_wait检测到的就绪事件,而不像 select和poll的数组参数那样既用于传入用户注册的事件, 又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率。 epoll对文件描述符的操作有两种模式:LT(电平触发)模式和ET(边沿触发)模式。LT模式是默认的工作模式, 这种模式下epoll相当于一个效率较高的poll。当往epoll内核事件表中注册一个文件描述符上的 EPOLLET事件时, epoll将以ET模式来操作该文件描述符。ET模式是epoll高效工作模式。 ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。 LT模式:只要socket上有未读完的数据,就会一直产生事件; ET模式:socket上每新来一次数据就会触发一次,如果上一次触发后,未将socket上的数据读完,也不会再触发,除非新来一次数据; */
epollserver.cpp:
#include<sys/types.h> #include<sys/msg.h> #include<sys/ipc.h> #include<sys/stat.h> #include<stdio.h> #include<string.h> #include<pthread.h> #include<stdlib.h> #include<unistd.h> #include<iostream> #include<sys/wait.h> #include<sys/socket.h> #include<sys/epoll.h> #include<sys/ipc.h> #include<errno.h> #include<sys/shm.h> #include<fcntl.h> #include<semaphore.h> #include<arpa/inet.h> #include<iostream> #include<assert.h> #include<ctype.h> #include<time.h> using namespace std; #define MS(a,b) memset(a,b,sizeof(a)) //将文件描述符设置成非阻塞的 int setnonblocking(int fd) { int oldt = fcntl(fd, F_GETFL); int newt = oldt | O_NONBLOCK; fcntl(fd, F_SETFL, newt); return oldt; } /*将文件描述符fd上的EPOLLIN注册到epollfd指示的epoll内核事件表中,参数flag指定是否对fd启用ET模式*/ void addfd(int epfd, int fd, bool flag) { epoll_event event; event.data.fd = fd; event.events = EPOLLIN; if (flag) { event.events |= EPOLLET; } epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); setnonblocking(fd); } //LT模式的工作流程 int LT(epoll_event* events, int num, int epfd, int listenfd) { char buf[10]; int i,sockfd; printf("LT num: %d\n", num); for (i=0; i<num; i++) { sockfd = events[i].data.fd; printf("LT sockfd: %d, listenfd: %d\n", sockfd, listenfd); if (sockfd == listenfd) { struct sockaddr_in cAddr; socklen_t cAddrLen = sizeof(cAddr); int connfd = accept(listenfd, (struct sockaddr*)&cAddr, &cAddrLen); addfd(epfd, connfd, false); //对connfd禁用ET模式 } else if (events[i].events & EPOLLIN) { /*只要socket上读缓存中还有未读出的数据,这段代码就被触发*/ printf("LT event trigger once\n"); MS(buf,'\0'); int ret = recv(sockfd, buf, 10-1, 0); if (ret < 0) { close(sockfd); continue; } printf("LT get %d bytes of content: %s\n", ret, buf); } else { printf("LT something error!"); } } } //ET模式的工作流程 int ET(epoll_event* events, int num, int epfd, int listenfd) { char buf[10]; int i,sockfd; printf("ET num: %d\n", num); for (i=0; i<num; i++) { sockfd = events[i].data.fd; printf("ET sockfd: %d, listenfd: %d\n", sockfd, listenfd); if (sockfd == listenfd) //有新的连接 { struct sockaddr_in cAddr; socklen_t cAddrlen = sizeof(cAddr); int connfd = accept(listenfd, (struct sockaddr*)&cAddr, &cAddrlen); addfd(epfd, connfd, true); } else if (events[i].events & EPOLLIN) //接收到数据,读socket { /*这段代码不会被重复触发,所以我们循环读取数据,以确保把socket读缓存中的所有数据读出*/ printf("ET event trigger once\n"); while(1) { MS(buf,'\0'); int ret =recv(sockfd, buf, 10-1, 0); if (ret < 0) { /*对于非阻塞IO,下面的条件成立表示数据已经全部读取完毕。此后,epoll就能再次触发sockfd上的EPOLLIN事件,以驱动下一次读操作*/ if(errno == EAGAIN || errno == EWOULDBLOCK) { printf("ET read later\n"); break; } close(sockfd); break; } else if (ret == 0) { close(sockfd); } else { printf("ET get %d bytes of content: %s\n", ret, buf); } } } else { printf("ET something error!"); } } } int main(int argc, char* argv[]) { if (argc <= 2) { printf("argc error!\n"); return -1; } const char* ip = argv[1]; int port = atoi(argv[2]); int ret = 0; struct sockaddr_in addr; bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; inet_pton(AF_INET, ip, &addr.sin_addr); addr.sin_port = htons(port); int listenfd = socket(PF_INET, SOCK_STREAM, 0); assert(listenfd >= 0); ret = bind(listenfd, (struct sockaddr*)&addr, sizeof(addr)); assert(ret != -1); ret = listen(listenfd, 5); assert(ret != -1); epoll_event events[1024]; int epfd = epoll_create(5); assert(epfd != -1); addfd(epfd, listenfd, true); while(1) { int ret = epoll_wait(epfd, events, 1024, -1); if (ret < 0) { printf("epoll fail!\n"); break; } // LT(events, ret, epfd, listenfd); /*使用LT模式*/ ET(events, ret, epfd, listenfd); /*使用ET模式*/ } close(listenfd); return 0; }
epollclient.cpp:
#include<sys/types.h> #include<sys/msg.h> #include<sys/ipc.h> #include<sys/stat.h> #include<stdio.h> #include<string.h> #include<pthread.h> #include<stdlib.h> #include<unistd.h> #include<iostream> #include<sys/wait.h> #include<sys/socket.h> #include<sys/epoll.h> #include<sys/ipc.h> #include<errno.h> #include<sys/shm.h> #include<fcntl.h> #include<semaphore.h> #include<arpa/inet.h> #include<iostream> #include<assert.h> #include<ctype.h> #include<time.h> using namespace std; #define MS(a,b) memset(a,b,sizeof(a)) int main(int argc, char* argv[]) { if (argc <= 2) { printf("argc error!\n"); return -1; } const char* ip = argv[1]; int port = atoi(argv[2]); int ret = 0; struct sockaddr_in addr; bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; inet_pton(AF_INET, ip, &addr.sin_addr); addr.sin_port = htons(port); int listenfd = socket(PF_INET, SOCK_STREAM, 0); assert(listenfd >= 0); printf("epoll client fd: %d\n", listenfd); ret = connect(listenfd, (struct sockaddr*)&addr, sizeof(addr)); assert(ret != -1); while(1) { char buf[128] = {0x00}; int i = 0,ret; sprintf(buf, "hanyufengloveliuyifei: %d",++i); ret = send(listenfd, buf, strlen(buf), 0); printf("ret: %d\n", ret); assert(ret >= 0); MS(buf,0); ssize_t len = recv(listenfd, buf, sizeof(buf)-1, 0); assert(len >= 0); printf("epoll client recv data: [%s]\n", buf); sleep(2); } return 0; }