2. EventHub源码分析:初始化、打开设备节点与epoll机制

发布时间:2026/10/8 7:24:32
2. EventHub源码分析:初始化、打开设备节点与epoll机制 2.1 EventHub的初始化流程EventHub的初始化说白了就是三件事打开输入设备、创建epoll句柄、注册监听。咱们直接看代码我挑关键部分讲。// EventHub.cpp 构造函数核心片段 EventHub::EventHub() : mEpollFd(epoll_create(EPOLL_SIZE_HINT)) { // 创建epoll实例 // 打开系统输入设备目录 mInputDeviceScan new InputDeviceScanner(); // 扫描 /dev/input/ 目录下的所有设备 scanDevicesLocked(); // 创建用于唤醒的pipe int wakeFds[2]; pipe(wakeFds); mWakeReadPipeFd wakeFds[0]; mWakeWritePipeFd wakeFds[1]; // 将wake pipe也加入epoll监听 epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, eventItem); }嗯这里要注意一个细节epoll_create的参数EPOLL_SIZE_HINT在Android中定义的是8。为什么是8我记得当时看代码时也疑惑过后来发现这其实只是个hint内核会动态扩展。说白了这个值设多大都行内核不鸟你。核心要点EventHub的构造函数做了三件关键事创建epoll实例mEpollFd扫描并打开所有输入设备节点创建wake pipe并注册到epoll2.2 打开设备节点scanDevicesLocked()接下来咱们看看设备节点是怎么打开的。这部分我建议你重点关注因为我在项目中遇到过好几次设备节点打开失败导致触摸屏不响应的问题。// 扫描设备的核心逻辑 status_t EventHub::scanDevicesLocked() { // 遍历 /dev/input/ 目录 DIR *dir opendir(/dev/input); if (dir NULL) return NO_INIT; struct dirent *entry; while ((entry readdir(dir)) ! NULL) { // 只处理 event 开头的设备节点 if (strncmp(entry-d_name, event, 5) 0) { char devPath[PATH_MAX]; snprintf(devPath, sizeof(devPath), /dev/input/%s, entry-d_name); // 打开设备节点 int fd open(devPath, O_RDWR | O_CLOEXEC); if (fd 0) { // 我曾经遇到过权限问题导致打开失败 // 特别是某些定制ROM会修改设备节点权限 continue; } // 创建设备信息并注册到epoll Device* device new Device(fd, entry-d_name); registerDeviceForEpoll(device); } } closedir(dir); return OK; }这里有个坑我必须要说。你看代码里用了O_CLOEXEC标志这个标志的作用是当进程执行exec()时自动关闭这个文件描述符。为什么要这么做我曾经在调试一个双屏异显项目时发现子进程莫名其妙持有了输入设备的fd导致主进程无法正常监听。查了两天才发现是忘记加O_CLOEXEC。嗯从那以后我写任何打开设备节点的代码都会习惯性加上这个标志。避坑指南设备节点打开失败时不要直接返回错误应该跳过继续扫描其他设备务必使用O_CLOEXEC标志防止fd泄露到子进程扫描完成后记得closedir()否则会造成文件描述符泄漏2.3 epoll机制事件监听的基石好设备节点打开了接下来就是怎么监听这些设备的事件。Android选择了epoll而不是select或poll。为什么你想想看一个Android设备可能有触摸屏、按键、鼠标、手柄等多个输入设备。如果用select每次都要遍历所有fd效率太低了。epoll的优势在于它只返回有事件发生的fd不需要遍历。咱们看看EventHub是怎么用epoll的// 注册设备到epoll void EventHub::registerDeviceForEpoll(Device* device) { struct epoll_event eventItem; memset(eventItem, 0, sizeof(eventItem)); eventItem.events EPOLLIN; // 只监听可读事件 eventItem.data.u32 device-id; // 添加到epoll实例 int result epoll_ctl(mEpollFd, EPOLL_CTL_ADD, device-fd, eventItem); if (result -1) { // 注册失败的处理 close(device-fd); delete device; } } // 等待事件的核心循环 int EventHub::getEvents(int timeoutMillis) { struct epoll_event eventItems[EPOLL_MAX_EVENTS]; // 阻塞等待事件timeoutMillis为超时时间 int eventCount epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis); if (eventCount 0) { // 被信号中断等情况 return -errno; } // 遍历所有有事件的fd for (int i 0; i eventCount; i) { uint32_t epollEvents eventItems[i].events; uint32_t deviceId eventItems[i].data.u32; // 处理事件... if (epollEvents EPOLLIN) { // 读取输入事件 readDeviceEvents(deviceId); } } return eventCount; }个人经验我在优化一个车载系统的触摸响应延迟时发现epoll_wait的超时时间设置很关键。如果设得太短CPU空转浪费电量设得太长触摸响应会有延迟感。Android默认的超时是-1无限等待但实际场景中我建议根据具体设备调整到50-100ms。2.4 epoll的边缘触发 vs 水平触发这里有个技术细节我觉得值得单独拿出来讲。epoll有两种触发模式边缘触发ET和水平触发LT。触发模式特点EventHub使用情况水平触发LT只要fd有数据可读每次epoll_wait都会返回✅ 默认使用边缘触发ET只在状态变化时通知一次需要一次性读完所有数据❌ 未使用为什么EventHub选择水平触发我个人理解是简单可靠。水平触发模式下即使你某次没读完数据下次epoll_wait还会通知你。而边缘触发要求你必须一次性读完否则就会丢失事件。对于输入系统这种对可靠性要求极高的场景水平触发是更稳妥的选择。我记得有一次团队里有个同事想优化性能把EventHub改成了边缘触发。结果呢触摸屏偶尔会丢事件用户反馈说「点一下没反应要点两下」。最后查出来就是边缘触发导致的事件丢失。嗯从那以后我再也不建议在输入系统里用边缘触发。2.5 wake机制如何优雅地中断epoll_wait最后咱们聊聊EventHub的wake机制。你可能会问epoll_wait是阻塞的如果我想主动唤醒它怎么办EventHub的做法是创建一个pipe把pipe的读端也注册到epoll中。当需要唤醒时往pipe的写端写入一个字节epoll_wait就会立即返回。// 唤醒epoll_wait void EventHub::wake() { // 写入一个字节到wake pipe const uint64_t ONE 1; ssize_t nWrite write(mWakeWritePipeFd, ONE, sizeof(ONE)); if (nWrite ! sizeof(ONE)) { // 写入失败的处理 } } // 消费wake事件 void EventHub::consumeWakeEvent() { uint64_t u64; ssize_t nRead read(mWakeReadPipeFd, u64, sizeof(u64)); // 读取并丢弃wake事件 }这个设计很巧妙。你想想看如果没有这个wake机制当InputReader想要通知EventHub「有新设备插入了」但EventHub正阻塞在epoll_wait上那怎么办有了wake pipeInputReader只需要写一个字节EventHub就能立即醒来处理新设备。总结一下EventHub的核心设计思想用epoll统一管理所有输入设备的fd实现高效的事件监听通过wake pipe实现主动唤醒打破epoll_wait的阻塞水平触发模式保证事件不丢失牺牲一点性能换取可靠性设备节点扫描采用「跳过失败」策略保证单个设备故障不影响整体好了EventHub的初始化、设备节点打开和epoll机制咱们就讲到这里。下一章我会深入分析EventHub是如何读取原始输入事件并转换成InputReader能理解的格式的。到时候咱们再聊。