介绍一下 Android Handler 中的 epoll 机制?
目录:
- IO 多路复用
- select、poll、epoll 对比
- epoll API
- epoll 使用示例
- Handler 中的 epoll 源码分析
IO 多路复用
IO 多路复用是一种同步 IO 模型,实现一个线程可以监视多个文件句柄。一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作,没有文件句柄就绪时会阻塞应用程序,交出 cpu。
与多进程和多线程技术相比,IO 多路复用技术的最大优势是系统开销小,系统不必为每个 IO 操作都创建进程或线程,也不必维护这些进程或线程,从而大大减小了系统的开销。
select、poll、epoll 就是 IO 多路复用三种实现方式。
select、poll、epoll 对比
select 最大连接数为进程文件描述符上限,一般为 1024;每次调用 select 拷贝 fd;轮询方式工作时间复杂度为 O(n)
poll 最大连接数无上限;每次调用 poll 拷贝 fd;轮询方式工作时间复杂度为 O(n)
epoll 最大连接数无上限;首次调用 epoll_ctl 拷贝 fd,调用 epoll_wait 时不拷贝;回调方式工作时间复杂度为 O(1)
epoll API
int epoll_create(int size);
创建 eventpoll 对象,并将 eventpoll 对象放到 epfd 对应的 file->private_data 上,返回一个 epfd,即 eventpoll 句柄。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) //返回值:成功 0;失败 -1
对一个 epfd 进行操作。op 表示要执行的操作,包括 EPOLL_CTL_ADD (添加)、EPOLL_CTL_DEL (删除)、EPOLL_CTL_MOD (修改);fd 表示被监听的文件描述符;event 表示要被监听的事件,包括:
- EPOLLIN(表示被监听的fd有可以读的数据)
- EPOLLOUT(表示被监听的fd有可以写的数据)
- EPOLLPRI(表示有可读的紧急数据)
- EPOLLERR(对应的fd发生异常)
- EPOLLHUP(对应的fd被挂断)
- EPOLLET(设置EPOLL为边缘触发)
- EPOLLONESHOT(只监听一次)
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) //返回值:监听到的产生的事件数
等待 epfd 监听的 fd 所产生对应的事件。epfd 表示 epoll句柄;events 表示回传处理事件的数组;maxevents 表示每次能处理的最大事件数;timeout:等待 IO 的超时时间,-1 表示一直阻塞直到来 IO 被唤醒,大于 0 表示阻塞指定的时间后被唤醒
epoll 使用示例
创建一个管道,使用 epoll 监听管道读端,然后进入阻塞:
int pipFd[2];
pipe(pipFd); //打开管道
struct epoll_event event;
event.data.fd = pipFd[0]; //设置为监听管道读端
event.events = EPOLLIN | EPOLLET; //设置参数,接收可以 read() 的通知
int epfd = epoll_create(256); //创建 epoll 对象
int res = epoll_ctl(epfd, EPOLL_CTL_ADD, pipFd[0], &event); //添加管道读端为要监听的文件描述符
struct epoll_event allEvs[256];
int count = epoll_wait(epfd, allEvs, 256, 5000); //当前线程进入阻塞,等待被唤醒
for(int i = 0; i //被唤醒,处理触发唤醒文件描述符
if(allEvs[i].data.fd == pipFd[0] && (allEvs[i].events & EPOLLIN)){
char buffer[256];
read(pipeFd, buffer, 100); //接收到管道可以进行读的信号,开始读取
}
}
在其他线程写入管道,通知唤醒:
write(pipFd[1], str,strlen("hello"));
eventfd
eventfd 是 Linux 系统中一个用来通知事件的文件描述符,基于内核向用户空间应用发送通知的机制,可以有效地被用来实现用户空间事件驱动的应用程序。
简而言之:eventfd 就是用来触发事件通知,它只有一个系统调用接口:
int eventfd(unsigned int initval, int flags);
表示打开一个 eventfd 文件并返回文件描述符,支持 epoll/poll/select 操作。
之所以要在介绍 Handler native 源码前先介绍 eventfd,是因为在 Android 6.0 后,Handler 底层替换为 eventfd/epoll 实现。而 6.0 之前是由 pipe/epoll 实现的,就像上面的 epoll 使用示例那样。
Handler 中的 epoll 源码分析
主要分析 MessageQueue.java 中的三个 native 函数:
private native static long nativeInit(); //返回 ptr
private native void nativePollOnce(long ptr, int timeoutMillis); //阻塞
private native static void nativeWake(long ptr); //唤醒
nativeInit
首先来看 nativeInit 方法,nativeInit 在 MessageQueue 构造函数中被调用,其返回了一个底层对象的指针:
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}
对应实现在 android_os_MessageQueue.cpp 中:
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
...
return reinterpret_cast(nativeMessageQueue);
}
可见 MessageQueue 对应的底层对象就是 NativeMessageQueue,而 NativeMessageQueue 初始化时会创建一个底层的 Looper 对象:
NativeMessageQueue::NativeMessageQueue() :
mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
mLooper = Looper::getForThread();
if (mLooper == NULL) {
mLooper = new Looper(false);
Looper::setForThread(mLooper);
}
}
如上代码,可以知道 Looper 对象是 ThreadLocal 类型。Looper 的构造函数如下:
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), ...{
mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
...
rebuildEpollLocked();
}
首先通过 eventfd 系统调用返回一个文件描述符,专门用于事件通知。接着来看 rebuildEpollLocked 方法:
void Looper::rebuildEpollLocked() {
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
struct epoll_event eventItem;
memset(& eventItem, 0, sizeof(epoll_event));
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeEventFd;
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
...
}
可以看到我们已经熟悉的 epoll 操作了:通过 epoll_create 创建 epoll 对象,然后调用 epoll_ctl 添加 mWakeEventFd 为要监听的文件描述符。
nativePollOnce
之前学习 Handler 机制时多次看到过 nativePollOnce 方法,也知道它会进入休眠,下面就来彻底搞懂它的原理。对应的底层调用同样是在 android_os_MessageQueue.cpp 中:
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
jlong ptr, jint timeoutMillis) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast(ptr);
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
mLooper->pollOnce(timeoutMillis);
...
}
可以看到实现同样是在 Looper.cpp 中,接着来看 Looper 的 pollOnce 方法:
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
for (;;) {
...
result = pollInner(timeoutMillis);
}
}
int Looper::pollInner(int timeoutMillis) {
...
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
...
至此通过调用 epoll_wait 方法,当前线程进入休眠,等待被唤醒。
nativeWake
最后来看如何通过 nativeWake 唤醒线程,首先是 android_os_MessageQueue.cpp 中:
static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast(ptr);
nativeMessageQueue->wake();
}void NativeMessageQueue::wake() {
mLooper->wake();
}
与 nativeInit、nativePollOnce 一样,最终实现都是在 Looper.cpp 中,Looper 的 wake 方法如下:
void Looper::wake() {
uint64_t inc = 1;
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
if (nWrite != sizeof(uint64_t)) {
if (errno != EAGAIN) {
LOG_ALWAYS_FATAL("Could not write wake signal to fd %d: %s",
mWakeEventFd, strerror(errno));
}
}
}
其中关键逻辑是对 mWakeEventFd 发起写入操作,从而唤醒 nativePollOnce 中通过 epoll_wait 进入休眠的线程。
推荐阅读:开源一组视频时间轴控件
Activity Window 创建及添加过程
抽象工厂模式
Android UI 绘制请求与绘制时机工厂模式Android 消息屏障与异步消息Java 并发编程知识点梳理总结关注我
助你升职加薪
Android 面试官
点赞在看, 年薪百万