select和poll在使用上方式类似,缺点较明显。而epoll是Linux特有的IO复用函数,在使用上与前两者有很大不同。epoll的工作原理可以概括为:三个函数,两种模式。三个函数是epoll_create(), epoll_ctl(),epoll_wait()。两种模式是 ET和LT
epoll_create()
int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。
调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体
epoll_ctl()
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同于select()是在监听事件时(epoll使用epoll_wait监听)告诉内核要监听什么
类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动
作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,对应结构体如下
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
其中events成员描述事件类型。epoll支持的事件类型和poll基本相同。表示epoll事件类型的宏是在poll对应的
宏前加上“E",比如epoll的数据可读事件是EPOLLIN。但epoll有两个额外的事件类型--EPOLLET
和EPOLLONESHOT.它们对于epoll的高效运作非常关键。data 成员用于存储用户数据,其类型epoll_data_t的定
义如下:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
epoll_wait()
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。
第一个参数表示内核中事件表的文件描述符;
参数events用来从内核得到事件的集合,如果检测到事件,就将所有就绪的事件从内核事件表复制到它指向的数组中,与select和poll不同;
maxevents告之内核这个events有多大,最多监听多少个事件,这个 maxevents的值不能大于创建epoll_create()时的size;
参数timeout是超时时间(毫秒,0会立即返回,-1是阻塞)。
该函数返回需要处理的事件数目,如返回0表示超时。
LT
LT(level triggered)是默认的工作方式,并且同时支持block和no-block socket。
内核告诉你一个文件描述符是否就绪了,然后可以对这个就绪的fd进行IO操作。
如果不作任何操作内核会继续通知,所以这种模式编程出错误可能性要小一点。
传统的select/poll都是这种模型的代表.
ET
ET (edge-triggered)是高速工作方式,只支持no-block socket。
在这种模式下当描述符从未就绪变为就绪时,内核通过epoll告诉你,并且不会再为那个文件描述符发送更多的就
绪通知,直到做了某些操作导致那个文件描述符不再为就绪状态了(比如发送,接收或者接收请求,或者发送接
收的数据少于一定量时导致了一个EWOULDBLOCK错误)。
如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。
epoll特点
(1)支持一个进程打开大数目的socket描述符(FD),它所支持的FD上限是最大可以打开文件的数目,这个数字在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max查看,一般来说这个数目和系统内存关系很大。
(2)IO 效率不随FD数目增加而线性下降。传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,任一时间只有部分的socket是"活跃"的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。
但是epoll不存在这个问题,它只会对"活跃"的socket进行 操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会。
在某些情况下,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率;相反效率还可能稍微的下降。
(3)使用mmap加速内核与用户空间的消息传递。这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核与用户空间mmap同一块内存实现的。
使用示例
#include <sys/epoll.h>
#include <stdlib.h>
#include <error.h>
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
//step1: epoll开始工作之前 先把文件描述符纳入epoll监管
struct epoll_event listen_fd;
listen_fd.data.fd=0;
listen_fd.events=EPOLLIN;
int epoll_fd=epoll_create(10);
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,0,&listen_fd);
//step2: epoll开始工作 阻塞的等待文件描述符就绪
struct epoll_event ready_events[10];
int ret=epoll_wait(epoll_fd,ready_events,10,-1);
//step3:epoll完成工作 看自己感兴趣的套接字是否就绪
if(ret<0)
{
cout<<"epoll error"<<endl; return -1;
}
else if(ret==0)
{cout<<"spoll time out";return -1;}
else
{
for(int i=0;i<ret;i++)
{
if(ready_events[i].data.fd==0)
{
char tmp[10];
read(0,tmp,10);
cout<<tmp<<endl;
}
}
}
}