EventHub对epoll的应用,本质上是用"统一等待队列"替换了"零散忙等" 。它的精妙不在于发明新用法,而在于将三类性质完全不同的FD(设备节点、文件系统事件、唤醒管道)纳入同一个epoll实例,用一套机制解决所有"等什么、什么时候等、被谁唤醒"的问题。
直接看源码骨架和运行时流,比纯文字描述更清晰。
🔧 一、核心数据结构与初始化(静力学结构)
EventHub构造函数中关于epoll的部分可拆解为1个实例 + 3个监听源 :
cpp
// frameworks/native/services/inputflinger/reader/EventHub.cpp
EventHub::EventHub(void) {
// 1. 创建唯一的epoll实例(内核中对应一个红黑树+就绪链表)
mEpollFd = epoll_create(EPOLL_SIZE_HINT); // 参数内核2.6.8后忽略,>0即可
// 2. 创建inotify实例,监听/dev/input目录增删
mINotifyFd = inotify_init();
inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);
// 3. **关键动作**:将inotify fd加入epoll
epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, {EPOLLIN, EPOLL_ID_INOTIFY});
// 4. 创建唤醒管道(pipe),本质是内核缓冲队列
pipe(wakeFds); // mWakeReadPipeFd / mWakeWritePipeFd
// 5. **关键动作**:将管道读端加入epoll
epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, {EPOLLIN, EPOLL_ID_WAKE});
// 6. 设备fd的加入不在这里,由openDeviceLocked()在扫描/热插拔时执行
}
关键理解:
EPOLL_ID_INOTIFY和EPOLL_ID_WAKE不是FD,是存储在epoll_event.data.u32的自定义标签 。epoll返回时,通过这个标签立刻知道是谁醒了,不需要遍历 。- 设备FD对应的
data.fd存的是设备FD本身,通过getDeviceByFdLocked()反查设备上下文 。
⚙️ 二、运行时核心:getEvents()中的epoll等待(动力学)
getEvents()是InputReader线程的主循环体,epoll_wait是它的心脏搏动点 。
cpp
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
for (;;) {
// ... 前置处理(设备移除/添加事件先返回)...
// ★★★ 核心阻塞点 ★★★
int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);
mPendingEventCount = size_t(pollResult);
mPendingEventIndex = 0; // 从头处理这批事件
// 处理本轮epoll返回的所有就绪FD
while (mPendingEventIndex < mPendingEventCount) {
const epoll_event& eventItem = mPendingEventItems[mPendingEventIndex++];
uint32_t epollId = eventItem.data.u32; // 或 data.fd
if (epollId == EPOLL_ID_INOTIFY) {
// 设备增删 → 调readNotifyLocked(),最终open/close设备
mPendingINotify = true;
} else if (epollId == EPOLL_ID_WAKE) {
// 主动唤醒(如IMS重配置),读空管道即可
awoken = true;
} else {
// ★ 普通输入设备有数据!
int fd = eventItem.data.fd;
if (eventItem.events & EPOLLIN) {
// 从设备节点read原始input_event
read(fd, readBuffer, sizeof(struct input_event) * capacity);
// 转换成RawEvent,填充到buffer返回上层
}
if (eventItem.events & EPOLLHUP) {
// 设备挂载点消失,标记关闭
}
}
}
// 如果buffer已满或有重要事件,return到InputReader
if (event - buffer > 0) return event - buffer;
}
}
核心优势:
- 一次等待,多个来源 :不论是有新的触摸报点、有人插拔USB键盘、还是SystemServer要求重新扫描,都在同一个
epoll_wait上醒来。避免了select/poll的FD集合全量传递,时间复杂度从O(n)降到近似O(1)(仅返回就绪的少量FD)。 - 边缘触发(隐含) :虽然EventHub设置为
EPOLLIN(电平触发),但read()设备节点时一般读完所有数据,符合边缘触发的处理习惯------不读完就一直触发。
🔌 三、设备插拔:inotify→epoll的回声链路
这是一个双栈监听的精巧设计 :
- 第一层(inotify) :
/dev/input目录被watch,当有新eventX节点创建或删除,内核向mINotifyFd写入inotify_event结构体。 - 第二层(epoll) :
mINotifyFd被epoll监听,当它可读时,epoll_wait返回。 - EventHub读到
IN_CREATE后,调用openDeviceLocked():open()设备节点获取FD- 再次调用
epoll_ctl(EPOLL_CTL_ADD)将此设备FD加入epoll - 至此,新设备也进入了"统一等待池"
等效图:
物理插入 → 内核创建设备节点 → inotify上报 → epoll发现inotify可读 → EventHub打开设备 → 设备FD加入epoll → 下次触摸事件直接通过设备FD唤醒epoll
整个过程无需轮询,完全事件驱动。
🚰 四、唤醒管道(wake pipe):epoll的自中断机制
epoll_wait是阻塞的,但某些场景需要主动打断等待 (例如IMS需要立即重新扫描设备)。EventHub没有使用pthread_cond_signal之类的跨线程信号,而是用管道写操作模拟FD就绪 :
cpp
void EventHub::wake() {
// 仅仅是向管道写入一个字节
write(mWakeWritePipeFd, "W", 1);
}
由于管道读端在epoll中监听了EPOLLIN,写入操作立即触发epoll_wait返回 ,且返回的事件ID为EPOLL_ID_WAKE。
为什么不用signal?
管道是FD,天然融入epoll模型,不干扰其他等待的FD,且线程安全。这是Linux下**自唤醒(self-pipe trick)**的经典实现。
📊 五、与原始poll/select的性能对比
| 特性 | epoll (EventHub实现) | poll/select |
|---|---|---|
| 监听FD数量 | 无上限(取决于系统) | FD_SETSIZE(1024)或线性扫描 |
| 就绪FD获取 | 仅返回就绪列表,O(K) | 全量传入传出,O(N) |
| 每次调用拷贝 | 仅首次epoll_ctl注册 | 每次调用都要拷贝全部FD集合 |
| 内核时间复杂度 | O(1)(回调机制) | O(N)(线性遍历) |
| EventHub特殊点 | 混合监听设备、inotify、管道 | 无法混合,需多个循环 |
数据佐证:
- 触控屏采样率可达240Hz,每个事件都需要从内核→EventHub。epoll的回调机制(内核就绪时直接放入链表)比poll每次重新线性扫描节省大量CPU 。
- Android 4.4之前使用poll,之后全面切换为epoll + wake pipe,就是为了解决高采样率下的性能瓶颈 。
🧠 六、总结一句话
EventHub的epoll本质是:将所有可等待资源(设备、目录、控制信号)统一抽象为"文件描述符",让内核充当仲裁者------谁就绪,谁上报,绝不空转。
这种设计使得InputReader线程可以在没有任何输入事件时完全休眠,不占CPU;事件爆发时瞬间唤醒,逐个消化,是Android触摸"跟手性"的底层基石之一。