epoll是Linux内核为处理大批量文件描述符而作了改进的epoll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
epoll的接口如下:
1. int epoll_create(int size);
创建一个epoll的 句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll 后,必须调用close()关闭,否则可能导致fd被耗尽。
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的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 */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的;
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
epoll的高效就在于,当我们调用epoll_ctl往里塞入百万个句柄时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已,如何能不高效?!
那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
最后看看epoll独有的两种模式LT和ET。无论是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。
这件事怎么做到的呢?当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait干了件事,就是检查这些socket,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回。而ET模式的句柄,除非有新中断到,即使socket上的事件没有处理完,也是不会次次从epoll_wait返回的。
epoll使用实例(从客户端收取消息):
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <poll.h>
#include <sys/epoll.h>
#define SIZE 100
int start_up(sockaddr_in &addr){
int socketi = socket(PF_INET, SOCK_STREAM, 0);
if (socketi < 0){
printf("%d, %s\n", errno, strerror(errno));
exit(1);
}
int flag = bind(socketi, (sockaddr*)&addr, sizeof(addr));
if (flag < 0){
printf("%d, %s\n", errno, strerror(errno));
exit(2);
}
flag = listen(socketi, 5);
if (flag < 0){
printf("%d, %s\n", errno, strerror(errno));
exit(2);
}
return socketi;
}//start_up
int main(int argc, char *argv[]){
if (argc < 3 ){
perror("can su error");
exit(0);
}
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(atoi(argv[2]));
addr.sin_family = PF_INET;
int socketi = start_up(addr);
/////////////////////////////////
int epfd = epoll_create(1024);
if (epfd < 0){
perror("epfd error");
exit(4);
}
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = socketi;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, socketi, &event) < 0){
printf("%d, %s\n", errno, strerror(errno));
exit(6);
}
int maxevents = 1;
int timeout = 2000;
struct sockaddr_in client;
socklen_t addr_len = sizeof(client);
char buf[100] = {0};
while (1){
struct epoll_event things[SIZE];
int ret = epoll_wait(epfd, things, maxevents, timeout);
for ( int i = 0; i < ret; i++){
if (things[i].data.fd == socketi){
int connfd = accept(socketi,(sockaddr*)&client, &addr_len);
if (connfd < 0){
printf("%d, %s\n", errno, strerror(errno));
continue;
}//if
event.data.fd = connfd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD,connfd, &event) < 0){
printf("%d, %s\n", errno, strerror(errno));
close(connfd);
}
maxevents++;
printf("加入一个\n");
continue;
}//if
ssize_t size = read(things[i].data.fd, buf, 20);
if (size <= 0){
event.data.fd = things[i].data.fd;
if ( epoll_ctl(epfd, EPOLL_CTL_DEL, things[i].data.fd, &event) < 0){
printf("del error\n");
}
maxevents--;
printf("client is quit\n");
}//if
else{
printf("client:%s\n", buf);
}//else
}//for
}//while
return 0;
}//main
以上信息参考:
http://blog.chinaunix.net/uid-24517549-id-4051156.html
http://blog.youkuaiyun.com/hdutigerkin/article/details/7517390
http://www.cnblogs.com/panfeng412/articles/2229095.html
http://blog.chinaunix.net/uid-17299695-id-3059110.html

epoll是Linux内核提供的一种高效处理大批量文件描述符的机制,它是select/poll的增强版本。epoll提供了水平触发和边缘触发两种模式,且在内核中通过红黑树和准备就绪链表来维护句柄。epoll_ctl用于注册和管理监听事件,epoll_wait则用于等待并返回就绪事件。这种设计提高了在高并发场景下的系统CPU利用率。
1985

被折叠的 条评论
为什么被折叠?



