epoll模型是服务器编程的高性能框架,比select 和 poll模型高效很多,当然还有其它的模型,如kqueue等,具体linux发行版提供不同的模型,一般都支持epoll吧。
epoll提供两种工作模式:LT 和 ET。
LT模式是epoll默认的工作方式,相当于一个效率很高的poll模型;而ET是高效的工作方式。
LT 和 ET本质的区别是:
LT模式状态时,主线程正在epoll_wait等待事件时,请求到了,epoll_wait返回后没有去处理请求(recv),那么下次epoll_wait时此请求还是会返回(立刻返回了);而ET模式状态下,这次没处理,下次epoll_wait时将不返回(所以我们应该每次一定要处理),可见很大程度降低了epoll的触发次数(记住这句话先)。
(所以,针对上面我对这个的高效的理解是:要看编程人员的实现方式,不是epoll一定高效,毕竟LT模式也可以在一次处理多次请求,或许是我没理解LT和ET底层还有什么数据结构、算法的差别吗,希望懂的同志指教一下!!)
其实,LT好理解,LT 模式下无论是否设置了EPOLLONESHOT,都是epoll_wait检测缓冲区有没有数据,有就返回,否则等待;
EPOLLONESHOT是在多线程环境应用的,试想如果主线程在epoll_wait返回了套接字conn,之后子线程1在处理conn,主线程回到epoll_wait,但还没等到子线程1返回conn又可读了,此时主线程epoll_wait返回,又分配给另一个线程,此时两个线程同时使用一个套接字,这当然是不行的,所以epoll模型定义了EPOLLONESHOT,意思就是设置了EPOLLONESHOT套接字在epoll_wait返回后,使用该线程的没重置此套接字前,即:
void gResetOneshot(int epollfd, int conn) { epoll_event event; event.data.fd = conn; event.events = EPOLLIN | EPOLLONESHOT; epoll_ctl(epollfd, EPOLL_CTL_MOD, conn, &event); }
下面程序看看(全文基于这服务端/客户端程序测试):
服务端:
建立epoll,然后将 监听套接字 和 连接套接字 均是LT模式(默认),代码稍微有点长,无所谓,其实很易懂
#include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <sys/socket.h> #include <string.h> #include <errno.h> #include <signal.h> #include <sys/epoll.h> #include <fcntl.h> int gSetNonblocking(int fd) { int old_option = fcntl(fd, F_GETFL); int new_option = old_option | O_NONBLOCK; fcntl(fd, F_SETFL, new_option); return old_option; } /* 往epoll描述符添加套接字 */ void gAddfd(int epollfd, int fd, bool oneshoot) { epoll_event event; event.data.fd = fd; event.events = EPOLLIN ; /* 同一时刻只允许一个线程处理该描述符 */ if (oneshoot) { event.events = event.events | EPOLLONESHOT; } epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event); gSetNonblocking(fd); } void gResetOneshot(int epollfd, int conn) { epoll_event event; event.data.fd = conn; event.events = EPOLLIN | EPOLLONESHOT; epoll_ctl(epollfd, EPOLL_CTL_MOD, conn, &event); } int main(int argc, char *argv[]) { int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) write(STDERR_FILENO, "socket error", 11); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(10002); addr.sin_addr.s_addr = INADDR_ANY; bind(sock, (struct sockaddr *)&addr, sizeof(addr)); listen(sock, 32767); signal(SIGPIPE, SIG_IGN); int epollfd; epollfd = epoll_create(5); gAddfd(epollfd, sock, false); int connfd; int number; epoll_event event[512]; while (1) { number = epoll_wait(epollfd, event, 512, -1); if (number < 0 && errno != EINTR) { printf("epoll failure\n"); break; } for (int i = 0; i < number; ++i) { int sockfd = event[i].data.fd; if (sockfd == sock && (event[i].events & EPOLLIN)) { struct sockaddr_in cliaddr; socklen_t clilen = sizeof(sockaddr_in); connfd = accept(sock, (struct sockaddr *)&cliaddr, &clilen); if (connfd < 0) { printf("errno is -> %d:%s\n", errno, strerror(errno)); continue; } /* 设置连接套接字EPOLLONESHOT */ gAddfd(epollfd, connfd, false); //gResetOneshot(epollfd, sock); printf("Client connect\n"); } /* 来子外界的信号,如在终端输入kill -signal PID给此进程时 */ else if (sockfd == connfd && (event[i].events & EPOLLIN)) { // printf("Don't process\n"); // gResetOneshot(epollfd, connfd); // continue; printf("Start sleep(10) ...\n"); sleep(10); char text[512]; int ret = recv(connfd, text, 512, 0); while (recv > 0) { if (ret > 0) { text[ret] = '\0'; printf("Recv(%d):%s\n", ret, text); } else if (ret == 0) { printf("Client close socket\n"); close(connfd); break; } else if (errno == EWOULDBLOCK) { printf("Wouldblock\n"); break; } else if (errno == EPIPE) { printf("Broken pipe\n"); break; } ret = recv(connfd, text, 512, 0); } //gResetOneshot(epollfd, connfd); } } } return 0; }
客户端没什么需要注意,测试用而已,也是使用epoll模型,也不关事,能连服务器测试就好,贴出来好对比一下而已
#include <unistd.h> #include <sys/socket.h> #include <sys/types.h> #include <stdio.h> #include <sys/un.h> #include <string.h> #include <arpa/inet.h> #include <errno.h> #include <stdlib.h> #include <signal.h> #include <sys/epoll.h> #include <fcntl.h> #define path "tempfile.socket" void sig(int sig) { char buf[512]; memset(buf, 0, sizeof(buf)); sprintf(buf, "write error, ertrno(%d) -> %s\n", errno, strerror(errno)); write(STDERR_FILENO, buf, strlen(buf)); sleep(3); exit(-1); } int main(int argc, char *argv[]) { if (argc != 2) { printf("Usage:./exec [port]\n"); exit(-1); } signal(SIGPIPE, sig); int sock = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(atoi(argv[1])); addr.sin_addr.s_addr = inet_addr("192.168.1.115"); connect(sock, (struct sockaddr *)&addr, sizeof(addr)); int old = fcntl(sock, F_GETFL); int newoption = old | O_NONBLOCK; fcntl(sock, F_SETFL, newoption); char buf[512]; memset(buf, 0, sizeof(buf)); int epollfd = epoll_create(5); epoll_event event[128]; epoll_event e1,e2; e1.data.fd = sock; e1.events = EPOLLIN | EPOLLET; epoll_ctl(epollfd, EPOLL_CTL_ADD, sock, &e1); e2.data.fd = STDIN_FILENO; e2.events = EPOLLIN | EPOLLET; epoll_ctl(epollfd, EPOLL_CTL_ADD, STDIN_FILENO, &e2); int nR; int ret; while (1) { int number = epoll_wait(epollfd, event, 128, -1); if (number <= 0) { printf("epoll_wait error\n"); break; } for (int i = 0; i < number; ++i) { if (event[i].data.fd == sock && (event[i].events & EPOLLIN)) { memset(buf, 0, sizeof(buf)); nR = read(sock, buf, 512); if (nR == 0) { close(sock); break; } write(STDOUT_FILENO, buf, nR); } else if (event[i].data.fd == STDIN_FILENO && (event[i].events & EPOLLIN)) { printf("please input string:"); fflush(stdout); memset(buf, 0, sizeof(buf)); nR = read(STDIN_FILENO, buf, 512); if (nR <= 0) { printf("errno[%d]:%s\n", errno, strerror(errno)); } ret = write(sock, buf, nR); if (ret == 0 && errno == EINTR) { printf("write sock error\n"); exit(-1); } else if (ret < 0) { printf("write sock ret < 0\n"); exit(-1); } printf("Send [%d]byte\n", ret); } } } close(sock); return 0; }
服务器在监听到连接套接字connfd有信息来时,epoll_wait返回,我设置了睡眠10秒,因为我想在recv之前在发送数据给服务端,测试结果如下:
因为server接受缓冲区(text)为512字节,可以容纳所有字符,所以一次输出了所有数据。
我再改一改程序,将recv那里的512改成5,然后我客户端每次发5个字节过去,结果如下:
先发送第一个信息(sadf),等到epoll_wait返回后,在发送两个,LT模式也能像ET模型那样处理数据,一样是减少了触发次数,我想问,ET高效的原因是???
ET模式要求使用非阻塞套接字,然后在处理请求时也是用一个while循环,直到没数据读就返回,就像是LT用的那个循环。
再回到上面那句话-> ET很大程度降低了触发次数(难道所谓的降低触发次数就是‘强逼’了程序员必须一次处理完所有请求?)! ,LT和ET的本质区别,ET到底比LT高效在哪?高效是模式高效,还是说 程序员 编程利用ET模式更有可能写出高效的服务器??我认为是后者,因为我找不到任何理由支持前者,了解的同志,再次提醒下知道的麻烦跟我说说,谢谢谢!!!
下面再来了解一下ET的其它特性 --->>> 什么是 这次不处理下次epoll_wait不再返回?
我们修改一下服务器程序,只修改一个if条件如下(注意,没有使用while):
else if (sockfd == connfd && (event[i].events & EPOLLIN)) { printf("Start sleep(10) ...\n"); sleep(10); char text[512]; /* 最多返回5个字符 */ int ret = recv(connfd, text, 5, 0); text[ret] = '\0'; printf("Recv(%d):%s\n", ret, text); }
①发送1条信息
②等epoll返回再发送2条信息
测试结果:
在epoll返回之后 和 recv之前,又发送了2条信息,然我第一次时我只处理一条数据,第二次epoll返回时也是处理一条数据,然后第三条数据并没有被处理。
所以,第三条信息属于没被立即处理的请求,由此,我们知道【调用epoll_wait后】 到【 epoll_waitf返回】 之间是一个信息集,一个信息集只通知返回一次;另外,每次【epoll_wait返回后】到【调用epoll_wait后】就来到的信息,又是一个信息集,必须一次全部处理,不然下次不返回。
再来验证,上面的客户端程序又发1条信息,结果如下:
信息PTYU被处理了,因为一个新的信息集来了,每来一个信息集就返回一次,当然,在处理过程中如果有新的请求(它的请求将会复制到套接字缓冲区),可以用while进行此次新请求处理也行的,下次就不返回了。
信息集是我自己起的称呼哈,其实我也觉得合场景,这个不管了!
最重要的是,ET到底比LT高效的原因?有见解的同志麻烦解释一下,在此谢过!