epoll与poll和select在使用过程中有很大区别,(1)epoll使用一组函数来来完成任务。它们分别是epoll_create(),epoll_clt和epoll_wait(),(2)再有epoll使用事件表来记录用户关心的描述符上的事件,由此不需要像select和poll一样把描述符集合(fd_set)和事件集(pollfd)在每次调用都要传入内核中,但是epoll需要额外维护一个内核时间表的描述符用于唯一标识内核中的其他事件表。(3)epoll提高了另外两个工作事件:EPOLLET(边沿触发的高效模式),EPOLLSHOT(多线程控制访问格式,类似于锁),稍后我们来看这两种模式。
我们还是用man手册来讲解epoll
epoll - I/O event notification facility epoll-->事件通知设备
#include <sys/epoll.h> 头文件
DESCRIPTION
epoll is a variant of poll(2) that can be used either as an edge-triggered or a level-triggered interface and scales well to large numbers of watched file descriptors. The following system calls are provided to create and manage an epoll instance:
epoll是poll的一个变种,它既能工作在边沿触发模式,也能作用于电平触发模式并且支持大数量的描述符监听。接下来的系统调用提供创建和管理epoll实例的方法。
* An epoll instance created by epoll_create(2), which returns a file descriptor referring to the epoll instance. (The more recent epoll_create1(2) extends the functionality of epoll_create(2).)
一个epoll实例被epoll_create创建,它返回一个指向epoll实例(内核事件表)的描述符。最新的epoll_create1()扩展了epoll_create,具体参加man 2 epoll_create)
epoll_create的函数原型
int epoll_create(int size);
size参数用于指定事件表的大小。该函数返回文件描述符来唯一标识所注册的内核事件表
man 手册关于epoll_create的描述
epoll_create() creates an epoll "instance", requesting the kernel to allocate an event backing store dimensioned for size descriptors.
The size is not the maximum size of the backing store(后备存储) but just a hint(暗示) to the kernel about how to dimension(划分) internal(外部) structures. (Nowadays, size is ignored; see NOTES below.)
比如下面的
63 int epfd=epoll_create(MAXFD);
64 assert(epfd!=-1);
* Interest in particular file descriptors is then registered via epoll_ctl(2). The set of file descriptors currently registered on an epoll instance is sometimes called an epoll set.
将感兴趣的描述符通过epoll_ctl进行注册,通过对于epoll实例(事件表)上的描述符就行注册也叫对于epoll设置
epoll_ctl函数原型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd是epoll_create创建的内核事件表的描述符
op是epoll操作
EPOLL_CTL_ADD 往事件表中注册事件
Register the target file descriptor fd on the epoll instance referred to by the file descriptor epfd and associate the event event with the internal file linked to fd.
EPOLL_CTL_MOD 修改fd上的事件
Change the event event associated with the target file descriptor fd.
EPOLL_CTL_DEL 删除fd上的事件
Remove (deregister) the target file descriptor fd from the epoll instance referred to by epfd. The event is ignored and can be NULL (but see BUGS below).
第三个参数 是是一个结构体
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 */
};
一般来说,我们只关注epoll_event data中的fd,即,将外部描述符添加到内核事件表中。epoll_data中的ptr用于指定与fd关联的数据。
events用于指定要注册的事件,这些事件与poll的基本相同,只不过在事件头加了一个大写的E,比如输入事件EPOLLIN。另外我们开头提到过
epoll还提供了EPOLLET(边沿触发模式),和EPLINESHOT用于控制多线程访问同一描述符
比如下面的添加fd的封装函数epoll_add();
38 void epoll_add(int epfd,int fd)
39 {
40 struct epoll_event ev;
41 ev.events=EPOLLIN;
42 ev.data.fd=fd;
43
44 if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1)
45 {
46 perror("epoll_ctl error");
47 }
48 }
* Finally, the actual wait is started by epoll_wait(2).
最后,真正的等待工作是由于epoll_wait开展的
epoll_wait函数原型
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd是时事件表描述符
events参数是一个数组,用于把所有就绪的事件从内核事件表(epoll instance)复制到events。这个数组这用于输出epoll_wait检测到的就绪事件(这些事件其实是位),而不必像select和poll那样的数组参数即用于传入注册事件(select的fd_set,pollfd 的events),又输出内核检测到的就绪事件(pollfd的revents).这样提高了epoll的效率
maxevents最大事件数
timeout:超时时间,毫秒为单位
The epoll_wait() system call waits for events on the epoll instance referred to by the file descriptor epfd. The memory area pointed to by events will contain the events that will be available for the caller. Up to maxevents are returned by epoll_wait().
The maxevents argument must be greater than zero.(maxevents必须大于0)
epoll_wait系统调用等待在epoll instance表(用epfd标识)中的保存描述符上产生对应的注册事件,
events指向的内存区域将存储调用函数所监听到的就绪事件,直到最大事件数被epoll_wait返回(此处有略微错误,请忽略)
The call waits for a maximum time of timeout milliseconds. Specifying a timeout of -1 makes epoll_wait() wait indefinitely, while specifying a timeout equal to zero makes epoll_wait() to return immediately even if no events are available (return code equal to zero).
该系统调用等待的最大时间为timeout指定的毫秒级别时间。如果置为-1那么epoll_wait就会一直阻塞,当将timeout置为0,那么epoll将会立即返回 即使没有就绪事件产生(返回值0)
68 struct epoll_event events[MAXFD]; //定义接收事件的结构体
69 while(1)
70 {
71 int n=epoll_wait(epfd,events,MAXFD,5000); //epoll调用
72 if(n<0)
73 {
74 perror("epoll wait error");
continue;
75 }
76 if(n==0) //超时或者当将timeout置为0,那么epoll将会立即返回 即使没有就绪事件产生(返回值0)
77 {
78 printf("timeout\n");
79 }
80 if(n>0) //有n个描述符就绪,这里就是epoll和poll,select在编程中的不同,poll,select在收到有多个描述符就绪后,需要扫描整个数组,就是MAXDFD,而epoll只需要扫描n次,因此查找次数变少了,效率就提高了
81 {
82 int i=0;
83 for(i=0;i<n;i++)
84 {
85 int fd=events[i].data.fd;
86 if(fd==-1)
87 {
88 continue;
89 }
90 if(events[i].events&EPOLLIN) //fd上发生了普通数据输入
91 {
92 if(fd==sockfd) //如果是监听套接字就创建连接,并把连接描述符加入到事件表中
93 {
94 struct sockaddr_in caddr;
95 int len=sizeof(caddr);
96 int c=accept(fd,(struct sockaddr*)&caddr,&len);
97 if(c<0)
98 {
99 continue;
100 }
101 printf("accept = %d\n",c);
102 epoll_add(epfd,c);
103 }
104 else
105 {
106 char buff[128]={0};
107 if((recv(events[i].data.fd,buff,127,0))<=0) //没有数据或者断开连接
108 {
109 epoll_del(epfd,events[i].data.fd); 必须先在时间表中删除在释放fd,不然会出错
110 close(fd);
111 printf("client over\n");
112 break;
113 }
114 printf("buff %s\n",buff);
115 send(events[i].data.fd,"ok",127,0);
116 }
117 }
118 }
epoll的ET模式和LT模式的对比
epoll的高效模式,是由边沿触发的,epll默认是LT模式,特别的为了试验明显 我们把接收的数据数目改一下,改为1,
即上面的if((recv(events[i].data.fd,buff,1,0))<=0) //没有数据或者断开连接,我们连接打开客户端,连接服务器,并发送hello,原来一次发送的数据,被多次去取了出来,这也体现了TCP流式套接字的特点。也就是默认的epoll模式下,数据只要在接收缓冲区,epoll就会不断的返回有数据就绪,要求服务器端接收到数据读完了。
下来,我们看一看ET模式下的情况
设置ET模式只需要在注册事件的时候位或一个EPOLLET就好
38 void epoll_add(int epfd,int fd)
39 {
40 struct epoll_event ev;
41 ev.events=EPOLLIN | EPOLLET;
42 ev.data.fd=fd;
43
44 if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1)
45 {
46 perror("epoll_ctl error");
47 }
48 }
运行看程序
可以看到:客户端在第一次输入hello之后,数据并没有一次性被读出来,只读出来一个h,接着timeout时间到了,打印timeout接着,客户端继续输入ABC,客户端发送过去之后打印的不是A,也不是ABC,而是e。也就是
说ET模式下,只提醒一次,后续的数据的读取要客户自己来接收,但是就是因为它只提醒一次,现在这一次之后数据缓冲区中还有的数据就不在被读出来,要下一次触发epoll事件才会被读出来,也就是说现在读或写事件因为没有后续事件而一直处于阻塞状态(饥饿),在此处ET模式在很大程度上降低了同一个epoll事件被重复触发次数因此ET模式效率要高。这体现的TCP流式报文的特点,也体现了epoll的高效性,提醒一次,应用程序可以不立即处理该事件,暂时就不调用epoll
从上面的情况我们知道一次epoll之后ser接收缓冲区中还有数据ello,并读堵塞,因此,我们在使用ET模式时应该将将描述符设置成非阻塞的。使用
#include<fcntl.h>
fcntl()调用
14 void setnoblock(int fd)
15 {
16 int oldfl=fcntl(fd,F_GETFL); //获得当前描述符的文件状态
17 int newfl=oldfl | O_NONBLOCK; //添加非阻塞标志
18 int oop=fcntl(fd,F_SETFL,newfl); //重新设置文件标志
19 if(oop==-1)
20 {
21 perror("fcntl error");
22 }
23 }
并在向内核事件表中添加描述符时,将描述符设置为非阻塞的
50 void epoll_add(int epfd,int fd)
51 {
52 struct epoll_event ev;
53 ev.events=EPOLLIN | EPOLLET;
54 ev.data.fd=fd;
55
56 if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1)
57 {
58 perror("epoll_ctl error");
59 }
60 setnoblock(fd);
61 }
并且还要修改一下我们在收到客户端数据的控制结构
118 while(1)
119 {
120 char buff[128]={0};
121 int num=recv(fd,buff,1,0);
122 if(num==-1) //数据读完了,发生没有数据读,但是还要的读的错误
123 {//read over
124 send(fd,"ok",2,0);
125 break;
126 }
127 else if(num==0) //断开连接
128 {
129 epoll_del(epfd,fd);
130 close(fd);
131 printf("client over\n");
132 break;
133 }
134 else //正常接收
135 {
136 printf("buff=%s\n",buff);
137 }
138 }
现在的情况是不是跟刚才不一样了,直接读出来所有数据,没有发生饥饿(阻塞),因此ET现在的ET模式才是真正的高效。但是你应该要明确一点,Io复用函数只关心描述符的状态,而与描述符的阻塞和非阻塞没关系。