当前位置: 首页 > 工具软件 > epoll > 使用案例 >

epoll用法详解与编程实例

呼延修然
2023-12-01

1 epoll使用的三个函数

使用epoll时会用到三个函数,因此把这个三个函数弄明白了,也就明白了epoll的用法。要明白这个三个函数,最重要的就是要明白函数的参数,明白需要什么样的参数以及每一个参数的含义。

1.1 epoll_create函数

【作用】:
这个函数的作用就是创建一个epoll对象,然后就可以它来监测多个I/O事件。
【入参】:
int size : 表示可以监听的I/O事件的最大个数
【返回值】:
int epfd:创建成功,返回一个大于0的值,代表epoll句柄,失败返回值小于0
当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

//头文件
// 使用epoll需要包含下面的头文件
        #include <sys/epoll.h>
       
		int epoll_create(int size); 
	
	一般用法:
		epfd = epoll_create(1);

1.2 epoll_ctl函数

【作用】:
向1.1中创建的epoll对象中添加/删除/修改需要监听的I/O事件。
【入参】:
int epfd:创建的epoll句柄
int op:表示操作类型。有三个宏表示:

/* 
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd。
*/

int fd:要监听的I/O事件(socket、实名管道)的对应的fd
struct epoll_event *event:表示已经事件的类型(读写等)(后面细讲)
【返回值】:
如果操作成功,返回值大于等于0,失败返回值小于0

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

一般用法:	
	epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, (struct epoll_event)&event);

1.3 epoll_wait函数

【作用】:
【入参】:
int epfd:1.1从创景的epoll对象的fd
events:用来存放从内核得到的就绪事件的集合;存放接收到的事件数组。
int maxevents:表示每次能处理的最大事件数,告之内核这个events有多大;这个maxevents的值不能大于创建epoll_create()时的size;
int timeout:是超时时间(填一个 t > 0:表示等待 t 毫秒ms;0:就会立即返回;-1: 阻塞)
【返回值】:
返回值ret小于0


	int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

一般用法:	
	struct epoll_event events[NUM];//事件数组
	int maxevents = NUM;
	ret = epoll_wait(epfd, events, maxevents, -1);//-1阻塞
	

2 epoll_event 结构体

在函数epoll_ctl以及函数epoll_wait的入参中都出现了这个结构体,这个结构体也是这3个函数中最复杂的参数类型。下面详细说明一下。

struct epoll_event {
	__uint32_t events; /* Epoll events */
	epoll_data_t data; /* User data variable */
};
/* *** 注意****
epoll_data 是一个联合体结构成员,所有的成员公用一段内存,因此实际上只能保留一个成员
它只会保存最后一个被赋值的成员,所以不要data.fd,data.ptr都赋值
*/
typedef union epoll_data {
	void *ptr;
	int fd;
	__uint32_t u32;
	__uint64_t u64;
} epoll_data_t;

epoll_event 结构体中有两个数据成员:events和data,其中events代表要监听的事件类型,data用来保存一写额外的数据类型,如监听时间的fd、回调函数等。

2.2 监听的事件类型

前面提到epoll_event 中events代表要监听的时间类型。
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET: 将EPOLL设为边缘触发(Edge Triggered:上升沿/下降沿)模式,即缓冲区数据从有(无)到无(有)。这是相对于水平触发(Level Triggered:高、低电平触发):缓冲区数据读没有读完,写没有写满。select()函数只支持水平触发。

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
实际上不同的监听事件是用不同的bit位来表示的,如果对应的bit位为1,表示代表了监听某事件,例如:

EPOLLIN 的值实际是为1,二进制为:0001 (最低位置1)
EPOLLOUT 的值实际是4,二进制为:0100  (第3位置1)
因此如果要同时监听读取事件,可以表示为
EPOLLIN | EPOLLOUT   -> 按位或的结果为 0101

2.3 epoll_wait如何返回就绪事件

注意到:int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
上面这个函数中涉及到epoll_event 这个结构体,这个结构体的作用就是用来保存返回的就绪事件

struct epoll_event p_events[NUM];//事件数组
int maxevents = NUM;
ret = epoll_wait(epfd, p_events, maxevents, -1);//-1阻塞
// 假如有5个就绪事件返回,即ret=5,那么返回的就绪事件信息就会保存在p_events数组的前5个位置,即
p_events[0] -- p_events[4]

再回过头来仔细想想,返回的events中为什么需要包含监听事件类型、fd等信息:
events成员:代表监听事件类型,因为需要就绪的事件是读还是写,以便分别采取不同的动作
data成员中fd:因为不同的事件采取的操作肯定是不同的,很多操作,例如读函数read中也会用到fd这个参数。因此fd是必要的。
那为什么data成员中有时还需要添加一些别的数据呢?
假如p_events里面值包含两个参数,那么在处理返回的就绪事件时可能需要向下面这么做:

struct epoll_event p_events[NUM];//事件数组
int maxevents = NUM;
ret = epoll_wait(epfd, p_events, maxevents, -1);//-1阻塞
for (int i = 0; i < ret; ++i) {
	int fd = p_events[i].data.fd; // 获取就绪事件的fd
	// 下面会根据不同的fd来进行不同的处理
	if (fd == fd_A) {
		// 按照fd_A 的处理方法处理
	}
	else if (fd == fd_B) {
		// 按照fd_B 的处理方法处理
	}
	...
	// 如果fd的类型太多的话,就会导致上面的判断语句十分冗余... 显然这不好
}
/* 那么有没有一个好的解决方法呢?   -- 提前为每个fd绑定一个回调函数
可以自定义一个结构体,这个结构体里面包含fd、回调函数、回调函数的参数等,
然后让data的中的ptr指针指向这个结构体,例如:
*/
struct my_events {
	int fd;
	void* arg;
	void (*call_back)(int fd,void *arg);
};
// 然后设置data中ptr指针指向my_events结构体:
struct my_events myEvents;
myEvents.fd = listenfd;               // 设置需要监听的fd
myEvents.call_back = call_func; // 设置对应的回调函数
myEvents.arg = arg;             //  设置回调函数参数
struct epoll_event epv = {0,{0}};
epv.data.ptr = &myEvents;
// 注册添加需要监听的事件
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, (struct epoll_event)&epv );
// 然后调用epoll_wait函数获取就绪事件之后就可以按照下面处理事件了:
struct epoll_event p_events[NUM];//事件数组
int maxevents = NUM;
ret = epoll_wait(epfd, p_events, maxevents, -1);//-1阻塞
for (int i = 0; i < ret; ++i) {
	my_events* p_myevents = p_events[i].data.ptr;
	int fd = p_myevents->fd;    // 获取就绪事件的fd
	void *arg = p_myevents->arg; // 获取就绪事件对应回调函数的参数
	// 直接调用回调函数处理就绪事件
	p_myevents->call_back(fd, arg);
}

通过上面的示例就可以发现,epoll_data 中data成员中ptr指针可以指向我们自定义的数据结构,在这个数据结构中,我们可以指定回调函数、回调函数参数等内容,这样我们获取就绪事件之后,就能够很方便的进行处理了。

【参考文章】
1、网络编程-epoll的相关API函数, epoll并发服务器
2、linux内核event原理,linux epoll epoll的原理;struct epoll_event 为什么要这样设计

 类似资料: