4. 事件驱动模型:epoll
4.1 epoll简介
(1)epoll是Linux内核为处理大批量的socket而改进的poll,相对于select/poll来说,epoll更加灵活。它使用一个文件描述符来管理多个socket。
(2)epoll之所以高效,是因为它将用户关心的socket事件存放到内核的一个事件表中。而不是像select/poll每次调用都需要重复传入fd_set。比如当一个事件发生(如读事件),epoll无须遍历整个被监听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入就绪队列的描述符集就行了。
4.2 epoll的工作机制
(1)调用epoll_create,内核会创建一个eventpoll结构体。该结构体中的rbr成员是一颗红黑树,存储着所有添加到epoll中需要监控的事件。而rdlist成员是一个双向链表,存放着将要通过epoll_wait返回给用户的满足条件的事件。
(2)调用epoll_ctl(),会向epoll对象中添加、删除或修改感兴趣的事件。
(3)当调用epoll_wait检查是否有事件发生时,内核会从eventpoll对象中的rdlist双向链表中检查是否有元素。如果有,则会把事件复制到用户态,同时将事件数量返回给用户。
4.3 epoll接口
(1)创建和关闭epoll对象
头文件 |
#include <sys/epoll.h> |
函数 |
int epoll_create(int size); int close(int epollfd); //关闭epoll对象 |
参数 |
size:用于告诉内核要监听的数目。注意,size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。 |
返回值 |
成功时返回epoll对象的文件描述符。失败返回-1 |
功能 |
创建一个epoll对象 |
(2)操作epoll对象
头文件 |
#include <sys/epoll.h> |
函数 |
int epoll_ctl (int epfd, int op, int fd, struct epoll_event* event); |
参数 |
epfd:由epoll_create()的返回值 op:表示要将fd添加到epoll对象或从epoll对象删除/修改fd等。 添加:EPOLL_CTL_ADD,删除:EPOLL_CTL_DEL,修改:EPOLL_CTL_MOD fd:需要监听的fd(文件描述符) struct epoll_event{ //告诉内核需要监听什么事件。 __uint32_t events; //如EPOLLIN,EPOLLOUT等。 epoll_data_t data; //用户自定义的数据 }; |
返回值 |
成功时返回epoll对象的文件描述符。失败返回-1 |
备注 |
struct epoll_event中的events成员变量的取值: (1)EPOLLIN:表示对应的文件描述符可以读(包括对端socket正常关闭) (2)EPOLLOUT:表示对应的文件描述符可写 (3)EPOLLPRI:表示对应的文件描述符有紧急的数据可读(带外数据的到来) (4)EPOLLERR:表示对应的文件描述符发生错误 (5)EPOLLHUP:表示对应的文件描述符被挂断 (6)EPOLLET:将epoll设置为边缘触发 (7)EPOLLONESHOT:只监听一次,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到epoll队列里。 |
(3)等待IO事件
头文件 |
#include <sys/epoll.h> |
函数 |
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout); |
参数 |
epfd:由epoll_create()的返回值 events:用来接收从内核得到事件的集合。 maxevents:最多返回多少个事件 timeout:超时时间(毫秒):0会立即返回。 |
返回值 |
成功时返回需要处理的事件数目,0表示超时 |
功能 |
等待IO事件 |
4.4 epoll的工作模式
(1)LT模式:默认模式,同时支持block和non-block socket。当epoll_wait检测到socket事件发生并将此事件通知应用程序,用户可以对这个就绪的socket进行IO操作,也可以,可以不立即处理该事件。如果你不作任何操作,在下次调用epoll_wait时,内核会继续通知应用程序。
(2)ET模式:边缘模式,只支持non-block socket。在这种模式下,只有当socket从未就结果变为就绪时,内核才会通知应用程序,以后就不再通知,直到应用程序对该socket做了某些操作,使得该socket不再处理就绪状态。值得注意的是,如果不对这个socket作IO操作,内核不会发送更多的通知(only once)
【编程实验】echo服务器
(1)主线程创建epoll对象,并注册listent socket的“读就绪”事件到epoll请求队列中。
(2)主线程调用epoll_wait等待事件发生。如果此时有用户连接进来,则会调知主线程,并调用我们设置的handle_accept函数。
(3)handle_accept函数中调用accept系统函数来接受请求,并将新的socket的“读就绪”事件插入epoll对象的请求队列中,等待客户端发送数据过来。
(4)如果服务器收到客户端发送的数据,主线程会从epoll_wait中返回,并将数据分派给handle_event函数去做(这里可以开启一个工作线程来完成!)
(5)handle_event接收并处理数据,然后准备好发送缓冲区,再注册“写就绪”事件。当系统检测到可以写数据后,就会调用sendData去发送数据。
(6)数据发送完后,重新注册“读就绪”事件,主线程调用epoll_wait等待客户端发送新的数据过来。
【注意】在Reactor模式中,没必要区分所谓的“读工作线程”和“写工作线程”。
//myevent.h
#ifndef __MYEVENT_H__ #define __MYEVENT_H__ typedef void (ev_callback)(int fd, int events, void* arg); typedef struct _tag_myevent { int fd; ev_callback* callback; int events; //事件类型(如EPOLLIN、EPOLLOUT等,也可按位或) void* arg; int status; //1:in epoll wait list, 0 not in; long last_active; //last active time char buff[512]; int len, s_offset; //标示发送和接收缓冲区当前的大小 }myevent_s; void copyData(myevent_s* src, myevent_s* obj); void event_set(myevent_s* ev, int fd, int events, ev_callback* callback); void event_add(int epollFd, myevent_s* ev); void event_del(int epollFd, myevent_s* ev); #endif
//myevent.c
#include "myevent.h" #include <memory.h> #include <time.h> #include <sys/epoll.h> //拷贝数据 void copyData(myevent_s* src, myevent_s* obj) { memcpy(obj->buff, src->buff, sizeof(obj->buff)); obj->len = src->len; obj->s_offset = src->s_offset; } //自定义事件的封装(包含事件发生时的回调函数、事件活跃时间等信息) void event_set(myevent_s* ev, int fd, int events, ev_callback* callback) { ev->fd = fd; ev->callback = callback; ev->events = events; ev->arg = ev; ev->status = 0; ev->last_active = time(NULL); memset(ev->buff, 0, sizeof(ev->buff)); ev->len = 0; ev->s_offset = 0; } //向epoll对象中添加或修改事件 void event_add(int epollFd, myevent_s* ev) { struct epoll_event epv={0, {0}}; int op; epv.data.ptr = ev; //用户数据,将自定义ev绑定到内核epoll_event上 epv.events = ev->events; op = (ev->status == 1) ? EPOLL_CTL_MOD : EPOLL_CTL_ADD; ev->status = 1; if(epoll_ctl(epollFd, op, ev->fd, &epv) < 0){ perror("event_add error"); } } //将指定的事件从epoll对象中删除 void event_del(int epollFd, myevent_s* ev) { struct epoll_event epv ={0, {0}}; if(ev->status != 1) return; //1: in epoll, 0: not in epv.data.ptr = ev; ev->status = 0; if(epoll_ctl(epollFd, EPOLL_CTL_DEL, ev->fd, &epv) < 0){ perror("event_del error"); } }
//epoll.c
#include "myevent.h" #include <sys/socket.h> #include <sys/epoll.h> #include <netdb.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <memory.h> #include <time.h> #define IPADDRESS "127.0.0.1" #define PORT 8888 #define MAX_EVENTS 100 //全局变量 int g_epollFd; myevent_s g_Events[MAX_EVENTS + 1]; //g_Events[MAX_EVENTS]用于保存listen fd /*函数声明*/ //设置为非阻塞模式 static int set_nonblock(int sockfd); //创建并绑定套接字 static int socket_bind(const char* ip, int port); //IO多路复用epoll static void do_epoll(int listenfd); //事件处理函数 static void handle_events(myevent_s* ev); //accept回调函数 static void handle_accept(int fd, int events, void* arg); //接收数据 static void recvData(int fd, int events, void* arg); //发送数据 static void sendData(int fd, int events, void* arg); int main(int argc, char* argv[]) { int listenfd; listenfd = socket_bind(IPADDRESS, PORT); //监听连接 listen(listenfd, 5); do_epoll(listenfd); return 0; } //设置为非阻塞模式 int set_nonblock(int sockfd) { int iret = -1; int opts; opts = fcntl(sockfd, F_GETFL); if(opts < 0){ perror("fcntl error"); return -1; } opts |= O_NONBLOCK; if((iret = fcntl(sockfd, F_SETFL, opts)) < 0){ perror("fcntl error"); } return iret; } //创建套接字并进行绑定 int socket_bind(const char* ip, int port) { int listenfd = -1; struct sockaddr_in servaddr; listenfd = socket(AF_INET, SOCK_STREAM, 0); if(listen < 0){ perror("socket error"); exit(1); } memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, ip, &servaddr.sin_addr); servaddr.sin_port = htons(port); if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){ perror("bind error"); exit(1); } // set_nonblock(listenfd); //设置为非阻塞模式 return listenfd; } //IO多路复用epoll void do_epoll(int listenfd) { //创建epoll对象 g_epollFd = epoll_create(MAX_EVENTS); //将listen socket加入到epoll对象中 event_set(&g_Events[MAX_EVENTS], listenfd, EPOLLIN, handle_accept); event_add(g_epollFd, &g_Events[MAX_EVENTS]); struct epoll_event events[MAX_EVENTS]; printf("server is running:%s(%d)\n", IPADDRESS, PORT); int checkPos = 0; while(1){ //1.检查是否超时(只检查前面的100个连接) long now = time(NULL); int i = 0; for(; i<100; i++, checkPos++){ //不检测查listen fd if(checkPos == MAX_EVENTS) checkPos = 0; if(g_Events[checkPos].status != 1) continue; long duration = now - g_Events[checkPos].last_active; if(duration >=60){ //设置不活动超过60秒为超时 close(g_Events[checkPos].fd); event_del(g_epollFd, &g_Events[checkPos]); printf("[fd=%d] timeout.\n", g_Events[checkPos].fd); } } //2.获取己经准备好的socket事件 int fds = epoll_wait(g_epollFd, events, MAX_EVENTS, 1000); //返回值为发生的事件数量 if(fds < 0){ perror("epoll_wait error, exit\n"); exit(1); } //处理事件 for(i=0; i<fds; i++){ myevent_s* ev = (myevent_s*)events[i].data.ptr; if((events[i].events & EPOLLIN) || (events[i].events & EPOLLOUT)) handle_events(ev); //可以将任务分派到新线程中处理。本例为简单起见,直接在主线程处理 } } //关闭epoll对象 close(g_epollFd); } //事件处理函数 void handle_events(myevent_s* ev) { ev->callback(ev->fd, ev->events, ev); } //接受客户端连接 void handle_accept(int fd, int events, void* arg) { struct sockaddr_in cliaddr; socklen_t len = sizeof(cliaddr); int nfd, i; //accept系统调用 if((nfd = accept(fd, (struct sockaddr*)&cliaddr, &len)) < 0){ perror("accept error"); return; } //检查连接数是否己经达到上限 do{ //使用do...while(0)是个技巧,可以代替goto //查找是否仍有可用连接数 for(i=0; i<MAX_EVENTS; i++){ if(g_Events[i].status == 0) break; } if(i == MAX_EVENTS){ //最后一个为listen fd printf("max connection limit[%d]\n", MAX_EVENTS); break; //跳出do...while } //找到可用连接,设置非阻塞模式 //if(set_nonblock(nfd) < 0) // break; //跳出do...while //添加一个与客户通信的socket描述符事件 event_set(&g_Events[i], nfd, EPOLLIN, recvData); event_add(g_epollFd, &g_Events[i]); }while(0); } //接收数据 void recvData(int fd, int events, void* arg) { myevent_s* ev = (myevent_s*)arg; int len; //接收数据 len = recv(fd, ev->buff + ev->len, sizeof(ev->buff) - ev->len, 0); event_del(g_epollFd, ev); if(len > 0){ ev->len += len; ev->buff[len] = '\0'; printf("Client[%d]:%s\n", fd, ev->buff); //设置写事件 myevent_s tmp; copyData(ev, &tmp); event_set(ev, fd, EPOLLOUT, sendData); copyData(&tmp, ev); event_add(g_epollFd, ev); }else if(len == 0){ close(ev->fd); printf("[fd=%d], closed gracefully.\n", fd); }else{ close(ev->fd); printf("recv [fd=%d] error.\n",fd); } } //发送数据 void sendData(int fd, int events, void* arg){ myevent_s* ev = (myevent_s*)arg; int len; //发送数据 len = send(fd, ev->buff + ev->s_offset, ev->len - ev->s_offset, 0); if(len > 0){ printf("send[fd=%d], [%d<->%d]%s\n", fd, len, ev->len, ev->buff); ev->s_offset += len; if(ev->s_offset == ev->len){ //设置读事件 event_del(g_epollFd, ev); event_set(ev, fd, EPOLLIN, recvData); event_add(g_epollFd, ev); } }else{ close(ev->fd); event_del(g_epollFd, ev); printf("send[fd=%d] error.\n", fd); } } /*输出结果 [root@localhost 15.AdvNet]# bin/epoll2 server is running:127.0.0.1(8888) Client[5]:abcdef send[fd=5], [512<->512]abcdef Client[5]:1234567 send[fd=5], [512<->512]1234567 ^C */
//echo_tcp_client.c(与上一例相同)
#include <netdb.h> #include <sys/socket.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <memory.h> int main(int argc, char* argv[]) { if(argc < 3){ printf("usage: %s ip port\n", argv[0]); exit(1); } /*步骤1: 创建socket(套接字)*/ int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0){ perror("socket error"); } //往servAddr中填入ip、port和地址族类型 struct sockaddr_in servAddr; memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_port = htons(atoi(argv[2])); //将ip地址转换成网络字节序后填入servAdd中 inet_pton(AF_INET, argv[1], &servAddr.sin_addr.s_addr); /*步骤2: 客户端调用connect函数连接到服务器端*/ if(connect(sockfd, (struct sockaddr*)&servAddr, sizeof(servAddr)) < 0){ perror("connect error"); exit(1); } /*步骤3: 调用自定义的协议处理函数和服务端进行双向通信*/ char buff[512]; size_t size; char* prompt = ">"; while(1){ memset(buff, 0, sizeof(buff)); write(STDOUT_FILENO, prompt, 1); size = read(STDIN_FILENO, buff, sizeof(buff)); if(size < 0) continue; buff[size-1] = '\0'; //将键盘输入的内容发送到服务端 if(write(sockfd, buff, sizeof(buff)) < 0){ perror("write error"); continue; }else{ memset(buff, 0, sizeof(buff)); //读取来自服务端的消息 if(read(sockfd, buff, sizeof(buff)) < 0){ perror("read error"); continue; }else{ printf("%s\n", buff); } } } /*关闭套接字*/ close(sockfd); }