手写Looper(二)---- 使用epoll
根据前面手写Looper(一)---- 使用eventfd了解的eventfd ,已经可以很容易实现Looper了。因为netive层的Looper就是一个简单的睡眠唤醒机制的实现,而eventfd本身来实现这个机制的。所以通过对eventfd包装一下就可以实现一个简单的Looper了。但eventfd还有一些缺陷,当一个线程阻塞在read方法中时,会一直阻塞下去,直到另一个线程写入内容。而我们还有超时的诉求,即read的时候设定一个超时时间,若是阻塞的时间超过了超时时间,则也唤醒的当前线程。这是考虑到我们会发送延迟消息,所以在延迟的时间到达后唤醒线程去处理事件。
epoll简介
epoll是一种事件通知机制,可以用来监控多个fd的读写状态。如前面了解的eventfd,若是存在多个eventfd,有一个线程的需求是监听这多个fd,不论哪个可读的时候,都去处理某些事件。我们就很难去做到上述的场景,或许可以创建eventfd的时候将其设置为非阻塞式的,然后循环read这几个fd,若是有可读的就去处理事件,否则继续循环读取。但这样实现有很大的问题,首先是CPU的问题,由于read是非阻塞式的,会导致程序在fd都不可读的时候一直处于循环中,耗费大量的CPU资源。其次,若是fd不是当前线程打开的,即无法控制其read是否是阻塞式的,上述方案也是不可行的。
epoll就是用来应对这种情况的,使用epoll可以同时监听多个fd,当添加其中的fd可读或者可写后就会唤醒epoll。同时也可以设置超时时间,超时后也会返回。
创建epoll
#include <sys/epoll.h>
int epoll_create (int __size)
int epoll_create1 (int __flags)
创建epoll有两个方法,第一个方法传入了size的大小,表示epoll可以监听的fd的个数,目前已经弃用,若仍使用的话,size会被舍弃,但是为了兼容旧版本,size的值不能是复数。第二个方法创建epoll可以传入flag,可以传入EPOLL_CLOEXEC
.
操作注册fd
#include <sys/epoll.h>
#define EPOLL_CTL_ADD 1
#define EPOLL_CTL_DEL 2
#define EPOLL_CTL_MOD 3
int epoll_ctl (int __epfd, int __op, int __fd, struct epoll_event *__event)
epoll增加、删除、修改fd都是通过epoll_ctl去操作的,其中参数__epfd是epoll实例,而参数__op代表具体的操作,是定义的三个常量,分别是EPOLL_CTL_ADD
、EPOLL_CTL_DEL
、EPOLL_CTL_MOD
,即当前操作是增加还是删除或者修改。第三个参数是需要操作的文件描述符。
第四个参数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_data_t data;
} __EPOLL_PACKED;
如上,其有两个字段。events代表的是监听的fd的事件操作,定义了一堆的枚举量,最常用的就是EPOLLIN
(可读时触发)和EPOLLOUT
(可写时触发)。第二个字段data是携带的参数,用于辨别当前触发的是哪个文件描述符。通常会将data.fd赋值为对应的fd。当epoll唤醒的时候,会将注册时传的event返回回来,需要通过event去判读是哪个fd唤醒的。
EPOLL_EVENT
enum EPOLL_EVENTS
{
EPOLLIN = 0x001, // fd可读时触发
#define EPOLLIN EPOLLIN
EPOLLOUT = 0x004, // fd可写时触发
#define EPOLLOUT EPOLLOUT
EPOLLET = 1u << 31 // 将epoll的触发方式设置为ET模式
#define EPOLLET EPOLLET
...
};
EPOLL_EVENT
是注册fd的时候需要设置的事件,用于设置当前注册的fd可以如何被触发唤醒。若是设置为了EPOLLIN,当文件可读的时候,会唤醒EPOLL。若是设置了EPOLLOUT,则当fd可写时唤醒EPOLL。
事件中还有一个比较重要的值,EPOLLET,即将触发方式设置为ET模式。EPOLL支持两种触发模式,level-triggeed(LT)
模式和edge-triggered(ET)
模式,EPOLL默认是LT模式。
LT
fd只要可读,就一直触发可读事件。fd只要可写入,就会一直触发可写事件。因此若是fd可读,需要将缓存区中的所有内容都读完,否则下次循环还会触发可读事件。同理,可写时也是如此。
ET
可读事件触发方式:1.fd从不可读变为可读时;2.fd缓存区内容增多时,即缓存区已有内容,然后又被写入了其他内容的时候会触发;3.fd缓存区不为空,并且通过EPOLL_CTL_MOD修改后出触发。可写事件的触发方式同理一样。
等待epoll返回
#include <sys/epoll.h>
int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);
epoll_wait
是阻塞式的,该方法会等待注册的fd进行唤醒。例如注册的fd是EPOLLIN,那么当fd可读的时候,epoll_wait
就会返回。第一个参数是epoll实例,第二个参数是一个数组,其中包含的是唤醒的fd对应的poll_event
,因为epoll是同时监听多个fd的。第三个参数是最大的event个数,通常是events数组的长度。最后一个参数是超时时间,单位毫秒,超过该时间后会直接唤醒,不论是否有fd注册的条件达到,设置为-1的话表示没有超时时间。
返回值是唤醒的fd的个数,此时会将fd对应的event写入events数组中,从第一个开始写入。因此唤醒后,需要根据返回值去遍历events,然后查看是哪个fd唤醒的。
epoll使用示例
#include <stdio.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <pthread.h>
#include <unistd.h>
#define MAX_COUNT 10
void *thread_write(void *data) {
int *eventfd = (int *) data;
eventfd_t ecount = 11;
// 子线程每隔1秒向eventfd中写入一个值用于将eventfd唤醒
for (int i = 0; i < 3; ++i) {
int res = eventfd_write(*eventfd, ecount);
if (!res) {
printf("write count: %zu\n", ecount);
}
sleep(1);
}
return NULL;
}
int main_epoll() {
// 创建epoll实例
int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
if (epoll_fd == -1) {
printf("create epoll error.\n");
return -1;
}
// 创建eventfd,使用epoll实现阻塞,eventfd就不需要再阻塞了
int eventfd_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (eventfd_fd == -1) {
printf("create efd error.\n");
return -1;
}
// 注册事件,监听可读事件
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = eventfd_fd;
int res = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, eventfd_fd, &event);
if (res) {
printf("epoll_ctl error.\n");
}
pthread_t thread;
res = pthread_create(&thread, 0, thread_write, &eventfd_fd);
if (res) {
printf("create thead fail.\n");
}
// 定义接收事件的event数组
struct epoll_event events[MAX_COUNT];
while (1) {
int count = epoll_wait(epoll_fd, events, MAX_COUNT, -1);
printf("wait count: %d\n", count);
if (count > 0) {
// 遍历唤醒的event,然后去判读是否是我们关注的可读事件
for (int i = 0; i < count; ++i) {
struct epoll_event tmp = events[i];
if (tmp.events == EPOLLIN) {
printf("fd: %d\n", tmp.data.fd);
// 默认时LT模式,所以需要将eventfd中的值读取出来,否则会一直触发可读事件
eventfd_t event_value;
eventfd_read(tmp.data.fd, &event_value);
} else {
printf("other event.\n");
}
}
} else {
printf("wait count zero.\n");
}
}
// 不会执行到这里,不过不要忘记关闭fd
close(eventfd_fd);
close(epoll_fd);
return 0;
}
总结
epoll类似于一个管家,它可以管理着多个文件描述符,当文件描述符的状态触发某个条件后,就会直接唤醒eoll的等待。epoll可以添加多个文件描述符,并且支持多种类型的fd,不仅仅是eventfd,像file、socketfd、pipe等都是可以的,并且可以设置超时唤醒。