epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次
epoll函数
epoll的三个操作函数:
int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
1. int epoll_create(int size);
创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好epoll句柄后,它就是会占⽤一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调⽤用close()关闭,否则可能导致fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,⽽是在这里先注册要监听的事件类型.
- epfd:是epoll_create()的返回值。
- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
- fd:是需要监听的fd(文件描述符)
- epoll_event:是告诉内核需要监听什么事,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);
等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
工作模式
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
1、LT模式
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
2、ET模式
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
epoll底层实现
epoll在底层实现了自己的高速缓存区,并且建立了一个红黑树用于存放socket,另外维护了一个链表用来存放准备就绪的事件。
执行epoll_ create时,创建了红黑树和就绪链表,执行epoll_ ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
epoll与poll和select相比
在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)
epoll的改进
epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。
看select的缺点I/O多路转接select
epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。来解决这个问题
对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,**而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。**epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果
对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
实现一个epoll模型
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
//events.data.ptr的指向
typedef struct fd_buff
{
int fd; //保存文件描述符
char buf[10240];//读写信息
}fd_buff_t, *fd_buff_p;
//创建一个struct fd_buff
static void *alloc_fd_buf(int fd)
{
fd_buff_p tmp = (fd_buff_p)malloc(sizeof(fd_buff_t));
if(!tmp)
{
perror("malloc");
return NULL;
}
tmp->fd = fd;
return tmp;
}
//获取listen_sock
int get_listen(char *ip, short port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock <0)
{
perror("socket");
exit(2);
}
int opt =1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr =inet_addr(ip);
//bind
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
exit(3);
}
//listen
if(listen(sock,10)<0)
{
perror("listen");
exit(4);
}
return sock;
}
int epoll_server(int listen_sock)
{
//1创建epoll模型
int epoll_fd = epoll_create(256);
if(epoll_fd < 0)
{
perror("epoll_create");
close(listen_sock);
return 5;
}
//2 添加监听读事件,
struct epoll_event ev;//要监听的事件
ev.events = EPOLLIN;
ev.data.ptr = alloc_fd_buf(listen_sock);
if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_sock,&ev)<0)//事件注册函数
{
perror("epoll_ctl");
return 6;
}
int nums = 0;
//int timeout = -1;//?阻塞式等待
int timeout = 1000;
struct epoll_event ready_evs[64];
while(1)
{
switch(nums = epoll_wait(epoll_fd,ready_evs,64,timeout))
{
case 0:
printf("timeout....\n");
break;
case -1:
perror("epoll_wait");
break;
default:
{
//监听事件已经就绪
int i =0;
for(; i < nums; ++i)
{
fd_buff_p fp = (fd_buff_p)ready_evs[i].data.ptr;
//listen_sock监听读事件就绪
if(fp->fd == listen_sock && \
(ready_evs[i].events & EPOLLIN))
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock,\
(struct sockaddr*)&client,&len);
if(new_sock < 0)
{
perror("accept");
continue;
}
printf("get a client:%s %d\n",\
inet_ntoa(client.sin_addr),ntohs(client.sin_port));
//将new_sock的写事件添加到模型中
ev.events = EPOLLIN;
ev.data.ptr = alloc_fd_buf(new_sock);
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,new_sock,&ev);
}
else if(fp->fd != listen_sock)
{
//其他监听文件描述符 读事件就绪
if(ready_evs[i].events & EPOLLIN)
{
ssize_t s = read(fp->fd,fp->buf,sizeof(fp->buf));
if(s >0)
{
fp->buf[s] = 0;
printf("client say#:%s",fp->buf);
//将其修改为写事件
ev.events = EPOLLOUT;
ev.data.ptr = fp;
epoll_ctl(epoll_fd,EPOLL_CTL_MOD,fp->fd,&ev);
}
else if(s <= 0)
{
printf("client quit\n");
close(fp->fd);
epoll_ctl(epoll_fd,EPOLL_CTL_DEL,fp->fd,NULL);
free(fp);
}
}
else if(ready_evs[i].events & EPOLLOUT)
{
//其他监听文件描述符 写事件就绪
char * msg = "HTTP/1.0 200 OK\r\n\r\n<html><h2>hello epoll! </h2></html>";
write(fp->fd,msg,strlen(msg));
close(fp->fd);
epoll_ctl(epoll_fd,EPOLL_CTL_DEL,fp->fd,NULL);
free(fp);
}
}
}
}
break;
}
}
return 8;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("Usge:%s [ip] [port]\n",argv[0]);
return 1;
}
int listen_sock = get_listen(argv[1],atoi(argv[2]));
epoll_server(listen_sock);
return 0;
}
本文深入解析epoll的工作原理,探讨其相较于select和poll的优势。包括epoll的核心函数介绍、两种工作模式及其底层实现机制。
1132

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



