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

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

  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触摸"跟手性"的底层基石之一。

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui