一、epoll简介
epoll是Linux操作系统中的一种可扩展的I/O事件通知机制,用于处理大量并发连接的网络编程场景。它在高性能网络服务器的开发中非常有用,因为它可以有效地管理大量的文件描述符,监视并等待这些文件描述符上的事件,并在事件发生时通知应用程序进行相应的处理。
相对于传统的I/O多路复用技术(如select和poll),epoll具有更高的效率和更好的扩展性。这是因为epoll使用了一种基于事件驱动的机制,可以避免遍历整个文件描述符集合,而是只关注活跃的文件描述符。这种机制允许应用程序只处理发生变化的事件,从而减少了系统调用的次数,提高了程序的效率。
二、select的局限性
(1) 文件描述符越多,性能越差。 单个进程中能够监视的文件描述符存在最大的数量,默认是1024(在linux内核头文件中定义有 #define _FD_SETSIZE 1024),当然也可以修改,但是文件描述符数量越多,性能越差。 (2)开销巨大 ,select需要复制大量的句柄数据结构,产生了巨大的开销(内核/用户空间内存拷贝问题)。 (3)select需要遍历整个句柄数组才能知道哪些句柄有事件。 (4)如果没有完成对一个已经就绪的文件描述符的IO操作,那么每次调用select还是会将这些文件描述符通知进程,即水平触发。 (5)poll使用链表保存监视的文件描述符,虽然没有了监视文件数量的限制,但是其他缺点依旧存在。 由于以上缺点,基于select模型的服务器程序,要达到十万以上的并发访问,是很难完成的。因此,epoll出场了。
三、epoll的优点
(1)不需要轮询所有的文件描述符 (2)每次取就绪集合,都在固定位置 (3)事件的就绪和IO触发可以异步解耦
四、epoll函数原型
4.1、epoll_create(int size)
代码语言:javascript复制#include <sys/epoll.h>
int epoll_create(int size);
功能:创建epoll的文件描述符。 参数说明:size表示内核需要监控的最大数量,但是这个参数内核已经不会用到,只要传入一个大于0的值即可。 当size<=0时,会直接返回不可用,这是历史原因保留下来的,最早的epoll_create是需要定义一次性就绪的最大数量;后来使用了链表以便便维护和扩展,就不再需要使用传入的参数。 返回:返回该对象的描述符,注意要使用 close 关闭该描述符。
4.2、epoll_ctl
代码语言:javascript复制#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// epoll_ctl对应系统调用sys_epoll_ctl
功能:操作epoll的文件描述符,主要是对epoll的红黑树节点进行操作,比如节点的增删改查。 参数说明:
参数 | 含义 |
---|---|
epfd | 通过 epoll_create 创建的文件描述符 |
op | 对红黑树的操作,比如节点的增加、修改、删除,分别对应EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL |
fd | 需要添加监听的文件描述符 |
event | 事件信息 |
4.2.1、event参数说明
struct epoll_event结构体原型
代码语言:javascript复制typedef union epoll_data{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64
};
struct epoll_event{
uint32_t events;
epoll_data_t data;
}
events成员代表要监听的epoll事件类型 events成员:
成员变量 | 含义 |
---|---|
EPOLLIN | 监听fd的读事件 |
EPOLLOUT | 监听fd的写事件 |
EPOLLRI | 监听紧急数据可读事件(带外数据到来) |
EPOLLRDHUP | 监听套接字关闭或半关闭事件 |
EPOLLET | 将EPOLL设为边缘触发(Edge Triggered)模式 |
data成员: data 成员是一个联合体类型,可以在调用 epoll_ctl 给 fd 添加/修改描述符监听的事件时携带一些数据,方便后面的epoll_wait可以取出信息使用。
4.2.2、扩展说明:SYSCALL_DEFINE数字 的宏定义
跟着的数字代表函数需要的参数数量,比如SYSCALL_DEFINE1代表函数需要一个参数、SYSCALL_DEFINE4代表函数需要4个参数。
4.2.3、注意
epoll_ctl是非阻塞的,不会被挂起。
4.3、epoll_wait
函数原型
代码语言:javascript复制#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:阻塞一段时间,等待事件发生 返回:返回事件数量,事件集添加到events数组中。也就是遍历红黑树中的双向链表,把双向链表中的节点数据拷贝出来,拷贝完毕后把节点从双向链表中移除。
返回值 | 含义 |
---|---|
大于0 | 事件个数 |
等于0 | 超时时间timeout到了 |
小于0 | 出错,可通过errno查看出错原因 |
参数说明:
参数 | 含义 |
---|---|
epfd | 通过 epoll_create 创建的文件描述符 |
events | 存放就绪的事件集合,是输出参数 |
maxevents | 最大可存放事件数量,events数组大小 |
timeout | 阻塞等待的时间长短,单位是毫秒,-1表示一直阻塞等待 |
五、epoll使用步骤
step 1:创建epoll文件描述符
代码语言:javascript复制int epfd = epoll_create(1);
step 2:创建struct epoll_event结构体
代码语言:javascript复制struct epoll_event ev;
ev.data.fd=listenfd;//保存监听的fd,以便epoll_wait的后续操作
ev.events=EPOLLIN;//设置监听fd的可读事件
step 3:添加事件监听
代码语言:javascript复制epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
step 4:等待事件
代码语言:javascript复制struct epoll_event events[EVENTS_LENGTH];
char rbuffer[MAX_BUFF]={
0 };
char wbuffer[MAX_BUFF]={
0 };
while(1)
{
int nready = epoll_wait(epfd,events,EVENTS_LENGTH,-1);//-1表示阻塞等待
int i=0;
for(i=0;i<nready;i )
{
int clientfd=events[i].data.fd;
if(clientfd==listenfd)
{
struct sockaddr_in client;
int len=sizeof(client);
int confd=accept(listenfd,(struct sockaddr*)&client,&len);
//step 2:创建struct epoll_event结构体
struct epoll_event evt;
evt.data.fd=confd;//保存监听的fd,以便epoll_wait的后续操作
evt.events=EPOLLIN;//设置监听fd的可读事件
// step 3:添加事件监听
epoll_ctl(epfd,EPOLL_CTL_ADD,confd,&evt);
}
else if(events[i].events &EPOLLIN)
{
int ret = recv(clientfd,rbuffer,MAX_BUFF,0);
if(ret>0)
{
rbuffer[ret]='