手写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等都是能够的,而且能够设置超时唤醒。