Epoll学习总结
中学历史老师告诉我,所有的简答题论述题都要回答三个问题,这个东西是什么?为什么?我们需要怎么做?
一、Epoll是什么?
epoll是Linux下多路复用IO接口select/poll的增强版本,是一个优秀的异步事件的通知机制,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
二、为什么使用Epoll?
支持一个进程打开大数目的socket描述符。select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是1024,远远不能满足处理上万个连接。Epoll的限制则是所支持的FD上限是最大可以打开文件的数目。
IO效率不随FD数目增加而线性下降。当并发量极大,而活跃量较小时,Select/poll会线性扫描所有的连接Fd,导致效率线性下降。EPoll只对活跃Socket操作。
强大的东西往往是简单的。epoll有2种工作方式:LT和ET。
LT水平触发是缺省的工作方式,同时支持block 和 nonblack socket. 水平触发方式下对就绪的 fd有需要处理通知,如果未处理,内核会继续通知,这种工作方式下编程较简单。
ET边沿触发方式,为高速工作方式,只支持non-block-socket。当描述符fd由未就绪变为就绪时,内核通过epoll通知,并且不会为该描述符发送更多通知,直到用户处理Epoll将文件描述符再次变回就绪状态。
总结ET与LT的区别,LT事件不会丢弃,buffer中有数据可读则不断通知,LT只要有事件未处理就会触发,ET只在事件发生之时通知。
三、Epoll怎样使用?
Epoll的系统调用。Epoll相关的系统调用包括epoll_create, epoll_ctl, epoll_wait. Epoll_create 创建一个epoll文件描述符, epoll_ctl用来添加/修改/删除需要侦听的文件描述符及其事件, epoll_wait/epoll_pwait接收发生在被侦听的描述符上的,用户感兴趣的IO事件。
通过在包含一个头文件#include <sys/epoll.h>
Ø epoll_create函数
函数声明:int epoll_create(int size)
该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围
首先通过int epfd = epoll_create(int maxfds)来创建一个epoll的句柄,其中maxfds为epoll所支持的最大句柄数。之后的所有操作将通过这个epfd句柄来进行操作。
Ø epoll_wait函数
函数声明:int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)
该函数用于轮询I/O事件的发生;
参数:
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值;
nfds = epoll_wait(epfd, events, maxevents, -1);
其中nfds 标示返回的待处理事件数量, epfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout是epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件范围,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则返回。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。
Ø epoll_ctl函数
函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
该函数用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。
参数: epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 删除
fd:关联的文件描述符;
event:指向epoll_event的指针;
如果调用成功返回0,不成功返回-1
四、举例及技巧
每次添加/修改/删除被侦听文件描述符都需要调用epoll_ctl,所以要尽量少地调用epoll_ctl,防止其所引来的开销抵消其带来的好处。有的时候,应用中可能存在大量的短连接(比如说Web服务器),epoll_ctl将被频繁地调用,可能成为这个系统的瓶颈。
epoll_wait范围之后应该是一个循环事件处理:
for( int n = 0; n < nfds; ++n)
{
if( events[n].data.fd == listenfd )
{
Clientfd = accept(listenfd, (struct sockaddr *) &local, &addrlen);
if(Clientfd < 0){
perror("accept");
continue; }
setnonblocking(Clientfd); // 将新连接置于非阻塞模式
ev.events = EPOLLIN | EPOLLET; // 并且将新连接也加入EPOLL的监听队列。如果要对写操作也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET
ev.data.fd = Clientfd;
if (epoll_ctl( epfd, EPOLL_CTL_ADD, Clientfd, &ev) < 0) {
// 设置好event之后,将这个新的event通过epoll_ctl加入到epoll的监听队列里面,这里用EPOLL_CTL_ADD来加一个新的epoll事件,通过EPOLL_CTL_DEL来减少一个epoll事件,通过EPOLL_CTL_MOD来改变一个事件的监听方式。
return -1;
}
}
else if( event[n].events & EPOLLIN )//如果是已经连接的用户,并且收到数据,那么进行读入
{
int sockfd_r;
if((sockfd_r = event[n].data.fd) < 0)
continue;
read(sockfd_r, buffer, MAXSIZE);
//修改sockfd_r上要处理的事件为EPOLLOUT
ev.data.fd = sockfd_r;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_r, &ev)
}
else if(event[n].events & EPOLLOUT)//如果有数据发送
{
int sockfd_w = events[n].data.fd;
write(sockfd_w, buffer, sizeof(buffer));
//修改sockfd_w上要处理的事件为EPOLLIN
ev.data.fd = sockfd_w;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_r, &ev)
}
}
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 被用于注册所感兴趣的事件和回传所发生待处理的事件,
epoll_data 联合体用来保存触发事件的某个文件描述符相关的数据,例如一个client连接到服务器,服务器通过调用accept函数可以得到与这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段以便后面的读写操作在这个文件描述符上进行
*ptr: 通过指针ptr携带应用层数据, 当事件的通知到来时,它不仅告诉你发生了什么样的事件,还同时告诉这次事件所操作的数据是哪些。
epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件可能的取值为:
EPOLLIN :表示对应的文件描述符可以读;
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:表示对应的文件描述符有事件发生;