Android EventHub的Epoll原理 笔记

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_INOTIFYEPOLL_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的回声链路

这是一个双栈监听的精巧设计 :

  1. 第一层(inotify)/dev/input目录被watch,当有新eventX节点创建或删除,内核向mINotifyFd写入inotify_event结构体。
  2. 第二层(epoll)mINotifyFd被epoll监听,当它可读时,epoll_wait返回。
  3. 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触摸"跟手性"的底层基石之一。

相关推荐
城东米粉儿2 小时前
Android音频系统 笔记
android
半切西瓜3 小时前
Android Studio 创建应用自动指定SDK目录
android·ide·android studio
RdoZam3 小时前
Android-封装个好用、轻量和通用的原生Adapter基类
android·kotlin
二流小码农3 小时前
鸿蒙开发:独立开发者的烦恼之icon图标选择
android·ios·harmonyos
独自破碎E4 小时前
BISHI43 讨厌鬼进货
android·java·开发语言
右手吉他4 小时前
Hostapd系统源代码学习
android
智先森zhi4 小时前
实战:将 Android 多Module应用迁移到 kmp+cmp
android·ios·kotlin
2501_937145415 小时前
IPTV电视源码系统2026优化版:技术升级,全场景流畅适配
android·电视盒子·源代码管理