一、背景
如果你看过 Android Looper.cpp 的代码,相信应该见过 eventfd 和 epoll 这两个陌生的函数。
# \system\core\libutils\Looper.cpp(Android 8.0 源码)
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),
mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
// 创建 eventfd 的句柄,返回该文件(Linux 中一切皆为文件)读写的描述符
mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
LOG_ALWAYS_FATAL_IF(mWakeEventFd < 0, "Could not make wake event fd: %s",
strerror(errno));
AutoMutex _l(mLock);
rebuildEpollLocked();
}
void Looper::rebuildEpollLocked() {
......
// 创建一个 epoll 的句柄,EPOLL_SIZE_HINT 是指监听的描述符个数
// 现在内核支持动态扩展,该值的意义仅仅是初次分配的 fd 个数,后面空间不够时会动态扩容。
// 当创建完 epoll 句柄后,占用一个 fd 值.
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
struct epoll_event eventItem;
memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeEventFd;
// 对 mWakeEventFd 文件描述符进行注册,这样 mEpollFd 就能监听到 mWakeEventFd 的读写事件。
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
......
}
int Looper::pollInner(int timeoutMillis) {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ pollOnce - waiting: timeoutMillis=%d", this, timeoutMillis);
#endif
......
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
// 等待 mEpollFd 上的 IO 事件
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
......
}
如果你对 eventfd 还不怎么了解,可以先看下这篇文章:通过实例来理解 eventfd 函数机制。
如果你对 epoll 也不怎么了解,可以先看下这篇文章:聊聊 Linux 五种 IO 模型。
之所以写这篇文章,主要是想看下 eventfd 的非阻塞模式在 epoll 中是怎么个表现形式,查了不少文章都没有说清楚,所以才想模拟 Looper.cpp 中的用法,看下究竟到底是怎么样的。
二、eventfd 和 epoll 的结合
这里以 Android 8.0 源码 Looper.cpp 作为参考,它的 write 操作时在 wake 函数中实现的:
void Looper::wake() {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ wake", this);
#endif
uint64_t inc = 1;
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
......
}
它的 read 操作时在 awoken 函数中实现的:
void Looper::awoken() {
#if DEBUG_POLL_AND_WAKE
ALOGD("%p ~ awoken", this);
#endif
uint64_t counter;
TEMP_FAILURE_RETRY(read(mWakeEventFd, &counter, sizeof(uint64_t)));
}
所以,用下面的代码进行模拟:
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <pthread.h>
#include <sys/eventfd.h>
#include <sys/epoll.h>
int event_fd = -1;
void *read_thread(void *dummy)
{
uint64_t inc = 1;
int ret = 0;
int i = 0;
for (; i < 2; i++) {
ret = write(event_fd, &inc, sizeof(uint64_t));
if (ret < 0) {
perror("child thread write event_fd fail.");
} else {
printf("child thread completed write %llu (0x%llx) to event_fd\n", (unsigned long long) inc, (unsigned long long) inc);
}
sleep(4);
}
}
int main(int argc, char *argv[])
{
int ret = 0;
pthread_t pid = 0;
event_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (event_fd < 0) {
perror("event_fd create fail.");
}
ret = pthread_create(&pid, NULL, read_thread, NULL);
if (ret < 0) {
perror("pthread create fail.");
}
uint64_t counter;
int epoll_fd = -1;
struct epoll_event events[16];
if (event_fd < 0)
{
printf("event_fd not inited.\n");
}
epoll_fd = epoll_create(8);
if (epoll_fd < 0)
{
perror("epoll_create fail:");
}
struct epoll_event read_event;
read_event.events = EPOLLIN;
read_event.data.fd = event_fd;
ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, event_fd, &read_event);
if (ret < 0) {
perror("epoll_ctl failed:");
}
while (1) {
printf("main thread epoll is waiting......\n");
ret = epoll_wait(epoll_fd, events, 16, 2000);
printf("main thread epoll_wait return ret : %d\n", ret);
if (ret > 0) {
int i = 0;
for (; i < ret; i++) {
int fd = events[i].data.fd;
if (fd == event_fd) {
uint32_t epollEvents = events[i].events;
if (epollEvents & EPOLLIN) {
ret = read(event_fd, &counter, sizeof(uint64_t));
if (ret < 0) {
printf("main thread read fail\n");
} else {
printf("main thread read %llu (0x%llx) from event_fd\n", (unsigned long long) counter, (unsigned long long) counter);
}
} else {
printf("main thread unexpected epoll events on event_fd\n");
}
}
}
} else if (ret == 0) {
printf("main thread epoll_wait timed out. continue epoll\n");
} else {
perror("main thread epoll_wait error.");
}
}
}
按照 Looper.cpp 源码,设置 eventfd 的计数器初始值为 0 且 flags 为 EFD_NONBLOCK | EFD_CLOEXEC。执行实例代码,结果如下(为了方便分析,让每一次写完阻塞 4 秒,epoll_wait 的超时时间为 2 秒):
wufan@Frank-Linux:~/Linux/test$ ./epoll_eventfd main thread epoll is waiting...... // main 线程阻塞在读端 child thread completed write 1 (0x1) to event_fd // 第一次写入后阻塞 4 秒 main thread epoll_wait return ret : 1 // 第一次写完后,立即唤醒 main 线程去进行读操作 main thread read 1 (0x1) from event_fd // main 线程读到了数据 main thread epoll is waiting...... // main 线程阻塞又在读端,超时时间为 2 秒 main thread epoll_wait return ret : 0 // main 线程阻塞等待时间到,返回 main thread epoll_wait timed out. continue epoll main thread epoll is waiting...... // main 线程阻塞又在读端,超时时间为 2 秒 child thread completed write 1 (0x1) to event_fd // // 第二次写入后阻塞 4 秒 main thread epoll_wait return ret : 1 // 第二次写完后,立即唤醒 main 线程去进行读操作 main thread read 1 (0x1) from event_fd // main 线程读到了数据 main thread epoll is waiting...... // main 线程阻塞又在读端,超时时间为 2 秒 main thread epoll_wait return ret : 0 // main 线程阻塞等待时间到,返回 main thread epoll_wait timed out. continue epoll main thread epoll is waiting...... // main 线程阻塞又在读端,超时时间为 2 秒 只要没有写入数据,就会在这个死循环中阻塞 -> 超时 -> 阻塞...
在通过实例来理解 eventfd 函数机制中,我们知道了 eventfd 的 EFD_NONBLOCK 模式下,读到计数器的值为 0 后,再继续读,会直接返回一个错误值,不会阻塞。但是上述的例子发现,eventfd 和 epoll 结合使用后,即使我将 flags 设置为 0 和上述执行的结果是一样的。这是为什么?因为按照 Looper.cpp 中的代码逻辑,分别对 epoll_wait 的返回值做了条件判断:
- ret > 0 说明有可读的值,才会去从 eventfd 中去读;
- ret == 0 说明超时,不会从 eventfd 中去读;
- ret < 0 说明 epoll 异常,不会从 eventfd 中去读;
好,那我们改一下代码,当 ret == 0 时,去执行一下 read 操作。同时将 flags 设置为 0:
while (1) {
......
} else if (ret == 0) {
int status = read(event_fd, &counter, sizeof(uint64_t));
printf("main thread epoll_wait timed out. continue epoll : %d\n", status);
} else {
perror("main thread epoll_wait error.");
}
}
wufan@Frank-Linux:~/Linux/test$ ./epoll_eventfd
main thread epoll is waiting......
child thread completed write 1 (0x1) to event_fd
main thread epoll_wait return ret : 1
main thread read 1 (0x1) from event_fd
main thread epoll is waiting......
main thread epoll_wait return ret : 0
child thread completed write 1 (0x1) to event_fd
main thread epoll_wait timed out. continue epoll : 8
main thread epoll is waiting......
main thread epoll_wait return ret : 0
一直阻塞在这儿
将 flags 设置为 EFD_NONBLOCK | EFD_CLOEXEC 时,就不会阻塞了。
三、小结
底层的知识我也不懂,也刚开始学,搜索了半天、看了不少文章,还是很容陷入一种似懂非懂、不确定的状态,可能这些知识对于懂 Linux 编程的人应该很 Easy。因此,特意将这个学习、验证的过程记录下来。
另外,Android Looper.cpp 中给 mWakeEventFd 设置 EFD_NONBLOCK,其实并没有发挥它真正地作用(也是在 epoll_wait 返回值 > 0 时才会去 read 操作)。不过,印象中在某处看到过(建议在创建 eventfd 时设置为非阻塞模式,可能是担心代码出问题了,一旦阻塞住了,出现卡死现象),不过,现在终于算是弄明白了!!!