一、基本概念
1、epoll初识
按照man手册的说法:epoll是为了处理大量句柄而改进了的poll
他是在2.5.44内核中被引进的,几乎具备了之前所说的select、poll的一切优点,被公认为是linux2.6下性能最好的多路I/O就绪通知方法。
2、epoll的相关系统调用
(1)epoll_create;
函数原型:
**参数说明:**size大小不是后备存储的最大大小,而是对内核如何标注内部结构的提示。但是在linux2.6.8以后,size参数是被忽略的,因为对于监控文件描述符的组织是红黑树,这个时候size实际上已经没有意义了
函数功能:该函数用来创建一个epoll句柄。
返回值:返回一个文件描述符
(2)epoll_ctl函数
函数原型:
函数功能:
epoll的时间注册函数
参数说明:
它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
(1)epfd是epoll_create()的返回值(epoll的句柄)
(2)第二个参数表示动作,用三个宏来表示
(3) 第三个参数是需要监听的fd
(4)第四个参数是告诉内核需要监听什么事.
第二个参数的取值:
EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd;
struct epoll_event结构:
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __attribute__ ((__packed__));
events可以是以下几个宏的集合:
事件 | 描述 |
---|---|
EPOLLIN | 表示对应的文件描述符可以读(包括对端SOCKET正常关闭) |
EPOLLOUT | 表示对应的文件描述符可以写 |
EPOLLPRI | 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来) |
EPOLLERR | 表示对应的文件描述符发生错误 |
EPOLLHUP | 表示对应的文件描述符被挂断 |
EPOLLET | 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered) 来说的 |
EPOLLONESHOT | 只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里 |
(3)epoll_wait()函数
函数原型:
函数功能:收集在epoll事件中已经发送的事件
参数说明:
(1)参数events是分配好的epoll_event结构体数组.,epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这 个events数组中,不会去帮助我们在用户态中分配内存)
(2)maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
(3)参数timeout是超时间 (毫秒,0会⽴立即返回,-1是永久阻塞).
返回值:
如果函数调⽤成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表 示函数失败.
3、总的理解epoll函数
在epoll函数句柄的创建过程中,epoll函数主要做了以下三件事情:
创建红黑树:红黑树结点内容,保存了用户想要告诉操作系统要监控的哪些文件描述符上的哪些事件。
创建就绪队列:在事件就绪后,操作系统将对应的文件描述符上的事件的结点放在就绪队列中,由用户检查就绪队列,来判断是否有事件就绪。
建立驱动到内核的回调机制:回调机制不需要操作系统一直在等,在事件就绪时,驱动会告诉操作系统,有事件就绪了,操作系统就会处理眼前的事件。这个回调机制在内核中称为epollcallback,它将发生的事件添加到rdlist
一张图让我们了解epoll的工作原理:
4、epoll函数的调用过程
epoll函数调用过程主要分为以下三步:
(1)调用epoll_create函数创建一个epoll句柄
(2)调用epoll_ctl函数将要监控的文件描述符进行注册。告诉操作系统用户关心的哪些文件描述符上的哪些事件
(3)调用epoll_wait函数返回给用户哪些文件描述符上的哪些事件就绪了
二、epoll的工作方式
epoll有两种工作方式:水平触发&边沿触发
1、水平触发(LT)—默认触发方式
当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
2、边沿触发(ET)
当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
三、简单的epoll服务器实现
程序代码:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<netinet/in.h>
5 #include<string.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<sys/types.h>
9 #include<sys/epoll.h>
10
11 #define MAX 128
12
13 int startUp(int port)
14 {
15 int sock=socket(AF_INET,SOCK_STREAM,0);
16 if(sock<0)
17 {
18 perror("socket");
19 exit(2);
20 }
21 int opt=1;
22 setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
23
24 struct sockaddr_in local;
25 local.sin_family=AF_INET;
26 local.sin_addr.s_addr=htonl(INADDR_ANY);
27 local.sin_port=htons(port);
28 if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
29 {
30 perror("bind");
31 exit(3);
32 }
33 if(listen(sock,5)<0)
34 {
35 perror("listen");
36 exit(4);
37 }
38 return sock;
39 }
40
41 void serviceIO(struct epoll_event*res,int num,int epfd,int listen_sock)
42 {
43 int i=0;
44 struct epoll_event ev;
45 for(i=0;i<num;i++)
46 {
47 //read & write
48 int fd=res[i].data.fd;
49 if(res[i].events&EPOLLIN)
50 {
51 //listen_sock&nomal
52 if(fd==listen_sock)
53 {
54 struct sockaddr_in client;
55 socklen_t len=sizeof(client);
56
57 int new_sock=accept(fd,(struct sockaddr*)&client,&len);
58 if(new_sock<0)
59 {
60 perror("accept");
61 continue;
62 }
63 printf("get new client [%s:%d]\n",inet_ntoa(client.sin_addr),\
64 ntohs(client.sin_port));
65 ev.events=EPOLLIN;
66 ev.data.fd=new_sock;
67 epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);
68 }
69 else
70 {
71 char buf[1024];
72 ssize_t s=read(fd,buf,sizeof(buf)-1);
73 if(s>0)
74 {
75 buf[s]=0;
76 printf("client:> %s\n",buf);
77
78 ev.events=EPOLLOUT;
79 ev.data.fd=fd;
80 epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);
81
82 }
83 else if(s==0)
84 {
85 printf("client quit!\n");
86 close(fd);
87 epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
88 }
89 else
90 {
91 perror("read");
92 close(fd);
93 epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
94 }
95 }
96 }
97 if(res[i].events & EPOLLOUT)
98 {
99 const char*msg="http/1.0 200 OK\r\n\r\n<html><h1>epoll server,hello!</h1></html> ";
100 write(fd,msg,strlen(msg));
101 close(fd);
102 epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
103 }
104 }
105 }
106 //./epoll_server 8080
107 int main(int argc,char*argv[])
108 {
109 if(argc!=2)
110 {
111 printf("Usage:%s[port]\n",argv[0]);
112 return 1;
113 }
114 //listen_sock
115 int listen_sock=startUp(atoi(argv[1]));
116 int epfd=epoll_create(256);
117 if(epfd<0)
118 {
119 perror("epoll_create");
120 return 5;
121 }
122
123 struct epoll_event ev;
124 ev.events=EPOLLIN;
125 ev.data.fd=listen_sock;
126 epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);
127 int timeout=-1;
128 struct epoll_event res[MAX];
129 int num=0;
130 for(;;)
131 {
132 switch(num=(epoll_wait(epfd,res,MAX,timeout)))
133 {
134 case -1:
135 perror("epoll_wait");
136 break;
137 case 0:
138 printf("timeout\n");
139 break;
140 default:
141 serviceIO(res,num,epfd,listen_sock);
142 break;
143 }
144 }
145 }
首先将epoll_server服务器程序跑起来:
然后远程登录连接:
服务器端收到发来的消息
四、epoll优点总结
(1)文件描述符数目无上限: 通过epoll_ctl()来注册一个文件描述符, 内核中使用红⿊黑树的数据结构来管 理所有需要监控的文件描述符.
(2)基于事件的就绪通知方式: 一旦被监听的某个文件描述符就绪, 内核会采用类似于callback的回调机制, 迅速激活这个文件描述符. 这样随着文件描述符数量的增加, 也不会影响判定就绪的性能;
(3) 维护就绪队列: 当文件描述符就绪, 就会被放到内核中的一个就绪队列中. 这样调用epoll_wait获取 就绪文件描述符的时候, 只要取队列中的元素即可, 操作的时间复杂度是O(1);