Android Handler epoll 机制分析

  1. IO 多路复用
  2. select、poll、epoll 对比
  3. epoll API
  4. epoll 使用示例
  5. eventfd
  6. Handler 中的 epoll 源码分析
    1. nativePollOnce
    2. nativePollOnce
    3. nativeWake

1. IO 多路复用

IO 多路复用是一种同步 IO 模型,实现一个线程可以监视多个文件句柄。一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作,没有文件句柄就绪时会阻塞应用程序,交出 cpu。

与多进程和多线程技术相比,IO 多路复用技术的最大优势是系统开销小,系统不必为每个 IO 操作都创建进程或线程,也不必维护这些进程或线程,从而大大减小了系统的开销。

select、poll、epoll 就是 IO 多路复用三种实现方式。

2. select、poll、epoll 对比

select 最大连接数为进程文件描述符上限,一般为 1024;每次调用 select 拷贝 fd;轮询方式工作时间复杂度为 O(n)

poll 最大连接数无上限;每次调用 poll 拷贝 fd;轮询方式工作时间复杂度为 O(n)

epoll 最大连接数无上限;首次调用 epoll_ctl 拷贝 fd,调用 epoll_wait 时不拷贝;回调方式工作时间复杂度为 O(1)

3. epoll API

1
int epoll_create(int size);

创建 eventpoll 对象,并将 eventpoll 对象放到 epfd 对应的 file->private_data 上,返回一个 epfd,即 eventpoll 句柄。

1
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(只监听一次)
1
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) //返回值:监听到的产生的事件数

等待 epfd 监听的 fd 所产生对应的事件。epfd 表示 epoll句柄;events 表示回传处理事件的数组;maxevents 表示每次能处理的最大事件数;timeout:等待 IO 的超时时间,-1 表示一直阻塞直到来 IO 被唤醒,大于 0 表示阻塞指定的时间后被唤醒

4. epoll 使用示例

创建一个管道,使用 epoll 监听管道读端,然后进入阻塞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 < count; i++){ //被唤醒,处理触发唤醒文件描述符
if(allEvs[i].data.fd == pipFd[0] && (allEvs[i].events & EPOLLIN)){
char buffer[256];
read(pipeFd, buffer, 100); //接收到管道可以进行读的信号,开始读取
}
}

在其他线程写入管道,通知唤醒:

1
write(pipFd[1], str,strlen("hello"));

5. eventfd

eventfd 是 Linux 系统中一个用来通知事件的文件描述符,基于内核向用户空间应用发送通知的机制,可以有效地被用来实现用户空间事件驱动的应用程序。

简而言之:eventfd 就是用来触发事件通知,它只有一个系统调用接口:

1
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 使用示例那样。

6. Handler 中的 epoll 源码分析

主要分析 MessageQueue.java 中的三个 native 函数:

1
2
3
private native static long nativeInit(); //返回 ptr
private native void nativePollOnce(long ptr, int timeoutMillis); //阻塞
private native static void nativeWake(long ptr); //唤醒
1. nativeInit

首先来看 nativeInit 方法,nativeInit 在 MessageQueue 构造函数中被调用,其返回了一个底层对象的指针:

1
2
3
4
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}

对应实现在 android_os_MessageQueue.cpp 中:

1
2
3
4
5
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
...
return reinterpret_cast<jlong>(nativeMessageQueue);
}

可见 MessageQueue 对应的底层对象就是 NativeMessageQueue,而 NativeMessageQueue 初始化时会创建一个底层的 Looper 对象:

1
2
3
4
5
6
7
8
NativeMessageQueue::NativeMessageQueue() :
mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
mLooper = Looper::getForThread();
if (mLooper == NULL) {
mLooper = new Looper(false);
Looper::setForThread(mLooper);
}
}

如上代码,可以知道 Looper 对象是 ThreadLocal 类型。Looper 的构造函数如下:

1
2
3
4
5
6
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), ...{
mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
...
rebuildEpollLocked();
}

首先通过 eventfd 系统调用返回一个文件描述符,专门用于事件通知。接着来看 rebuildEpollLocked 方法:

1
2
3
4
5
6
7
8
9
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 为要监听的文件描述符。

2. nativePollOnce

之前学习 Handler 机制时多次看到过 nativePollOnce 方法,也知道它会进入休眠,下面就来彻底搞懂它的原理。对应的底层调用同样是在 android_os_MessageQueue.cpp 中:

1
2
3
4
5
6
7
8
9
10
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
jlong ptr, jint timeoutMillis) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
mLooper->pollOnce(timeoutMillis);
...
}

可以看到实现同样是在 Looper.cpp 中,接着来看 Looper 的 pollOnce 方法:

1
2
3
4
5
6
7
8
9
10
11
12
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 方法,当前线程进入休眠,等待被唤醒。

3. nativeWake

最后来看如何通过 nativeWake 唤醒线程,首先是 android_os_MessageQueue.cpp 中:

1
2
3
4
5
6
7
8
static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->wake();
}

void NativeMessageQueue::wake() {
mLooper->wake();
}

与 nativeInit、nativePollOnce 一样,最终实现都是在 Looper.cpp 中,Looper 的 wake 方法如下:

1
2
3
4
5
6
7
8
9
10
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 进入休眠的线程。