Android Framework的输入输出系统 epoll 和 inotify

8 阅读4分钟

输入输出的源头

  • 在Android系统中,当我们按下按键或滑动屏幕的时候,系统是怎么知道这些操作的呢,这一切的源头都是/dev/input目录下的设备节点

额外知识点

  • FD是File Descriptor的缩写 是文件描述符
  • 在Linux中,万物皆文件,不管是照片、键盘、屏幕、网络,系统都给他们看作为文件
    • 所以当你想要操作一个文件的的时候,比如读取键盘按键,你不能直接抓着硬件操作
    • 你应该要告诉内核,我要打开那个键盘设备
    • 如果内核同意你打开,就会给你发送一个非负整数
    • 这个数字就是FD。之后你找内核办事,这个数字给到内核,内核就知道你指的是哪个设备了
  • 常见的固定FD
    • 0 标准输入 —— 通常键盘
    • 1 标准输出 —— 通常指屏幕
    • 2 标准错误 —— 报错信息的输出

inotify epoll

  • inotify 负责监控设备的增加与删除
  • epoll 负责监控设备数据的产生

为什么需要inotify呢

  • 在Linux中,万物皆是文件,每一个输入设备在插入的时候,都会在/dev/input/目录下生成一个event0event1这样的文件
  • 那程序什么时候知道多了一个event文件呢,如果用死循环不断的扫描目录,那么会极大的消耗CPU资源
  • inotifyiu可以解决这个问题
    • 他是一个异步文件系统监控机制,它允许应用程序监控文件系统事件(如创建、删除、移动),并在事件发生时通过读取文件描述符的方式告知用户

为什么需要epoll呢

  • 一旦设备文件创建好了,那么问题是哪个设备有数据了?
  • 假如说有10个输入设备
  • 传统的select/poll 每次都要把10个文件描述符传给内核,内核轮询一边后告诉你有几个可读,你还得自己再找一遍具体是哪一个。时间复杂度是O(n)
  • epoll 是采用事件触发机制 只需把要监控的设备告诉内核。检测到事件后 事件查询红黑树查询。内核会在硬件产生数据的时候将其放入就绪链表,调用epoll_wait时 内核直接返回就绪的文件 时间复杂度是O(1)

inotify机制

核心流程

  • inotify_init():创建一个inotify实力 返回一个文件描述符
  • inotify_add_watch:告诉内核你要监控哪个目录 哪些动作
  • read(fd, ...):这是一个阻塞操作。当文件发生变化的时候,能读出一个或多个inotify_event结构体

示例代码

#include <sys/inotify.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

void handle_events(int fd) {
    char buf[1024];
    // 1. 读取事件(注意:read 会阻塞直到有事件发生)
    int len = read(fd, buf, sizeof(buf));
    
    int pos = 0;
    while (pos < len) {
        struct inotify_event *event = (struct inotify_event *)&buf[pos];
        
        if (event->len) { // 如果有文件名
            if (event->mask & IN_CREATE) printf("检测到新设备: %s\n", event->name);
            if (event->mask & IN_DELETE) printf("设备已移除: %s\n", event->name);
        }
        // 结构体长度 + 字符串长度 = 下一个事件的偏移量
        pos += sizeof(struct inotify_event) + event->len;
    }
}

int main(int argc, char **argv) {
    if (argc != 2) {
        printf("用法: %s <监控目录>\n", argv[0]);
        return -1;
    }

    int mINotifyFd = inotify_init();
    // 监控创建和删除动作
    inotify_add_watch(mINotifyFd, argv[1], IN_CREATE | IN_DELETE);

    while (1) {
        handle_events(mINotifyFd);
    }
    return 0;
}

epoll机制

核心流程

  • epoll_create(size):创建epoll句柄
  • epoll_ctl(epfd, op, fd, event):注册、修改或者删除的FD
  • epoll_wait(epfd, events, max, timeout):等待事件发生,返回就绪的FD的数量

示例代码

#include <sys/epoll.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv) {
    int epollFd = epoll_create(8);
    struct epoll_event eventItem;
    struct epoll_event mPendingEventItems[16];

    // 将命令行传入的所有文件加入监控
    for (int i = 1; i < argc; i++) {
        int fd = open(argv[i], O_RDWR);
        eventItem.events = EPOLLIN; // 关注“可读”事件
        eventItem.data.fd = fd;
        epoll_ctl(epollFd, EPOLL_CTL_ADD, fd, &eventItem);
    }

    while (1) {
        // 等待事件,-1 表示无限等待
        int count = epoll_wait(epollFd, mPendingEventItems, 16, -1);
        for (int i = 0; i < count; i++) {
            char buf[512];
            int len = read(mPendingEventItems[i].data.fd, buf, sizeof(buf));
            buf[len] = '\0';
            printf("从 FD %d 获取数据: %s\n", mPendingEventItems[i].data.fd, buf);
        }
    }
}

EventHub

  • 在Android Framework中,EventHub会同时开启inotify和epoll
    • inotify监控/dev/input
    • inotify发现新文件创建时,调用open打开它,并将其FD加入epoll的监控队列
    • inotofy发现文件删除时,将其FD从epoll队列中移除
    • epoll_wait统一管理所有的FD 包括inotify自己的FD和所有设备文件的FD

总结

  • inotify解决了输入设备动态识别的问题,避免了无效的目录循环 轮询
  • epoll解决了多路复用”的性能问题,保证了在有大量输入设备时,系统依然能以O(1)的效率响