Android Input 学习-INotify与Epoll机制

使用背景

Android Input需要使用InputReader去监控设备节点的一些动作,包括节点的新建和删除动作以及如何去确定节点中是否有内容可以去读.最简单的方法是起一个线程在循环中不断地去做轮询(polling),但是这样做的效率比较低,而且会导致设备的电量在无意义的轮询中消耗掉.众所周知Android使用的Linux内核,因此面对这种问题,Android使用了Linux提供的INotify和Epoll机制,异步的去读取消息,而不是一直在轮询.

1. INotify机制

  • INotify是Linux提供的用于检测文件系统变化的通知机制,INotify可以用于检测单个文件,也可以用于检测整个目录,当检测的对象是一个目录的时候,目录本身和目录里的内容都会成为检测的对象.
  • INotify机制用两个基本对象,分别是inotify对象和watch对象,都是用文件描述符表示.
  • inotify对象对应一个队列,应用程序可以往inotify对象添加多个监听.当监听的事件发生的时候,可以通过read()函数(read函数是一个阻塞函数)从inotify对象中将事件信息读取出来.inotify创建方式:
int inotifyfd = inotify_init();
  • watch对象是用来描述文件系统的变化事件的监听.它是一个二元组,包括监听目标和事件掩码两个元素.监听目标是文件系统的路径,包括文件夹和文件.事件掩码则表示了要去监听的事件类型,包括文件的创建(IN_CREATE)与删除 (IN_DELETE).watch对象的创建如下:
int wd = inotify_add_watch(inotifyfd, "/dev/input",  IN_CREATE | IN_DELETE);

以上代码完成后,当/dev/input下的设备节点发生创建与删除操作的时候,会将相应的信息写入到inotifyfd所描述的inotify对象中,此时就可以通过read()函数从inotifyfd描述符中将事件信息读取出来.
事件信息使用的结构体inotify_event 如下:

struct inotify_event {
            __s32        wd; /*watch descriptor,事件对应watch对象的描述符*/
            __u32        mask; /*watch mask,事件类型,就是我们需要监听的,例如文件创建的话,此时的mask就是IN_CREATE*/
            __u32        cookie; /*cookie to sychronize two events*/
            __u32        len; /*length (including nulls) of name,name字段的长度*/
            char         name[0]; /*stub for possibale name,name字段的长度是0,也就是说是可变长的,用于存储产生此事件的文件路径*/
};

当监听事件发生事件,可以通过read()函数将一个或多个未读取的事件信息读取出来:

struct inotify_event *event;
char event_buf[512];
event = read(inotifyfd, event_buf, sizeof(event_buf));

能够读取的事件数量取决于数组的长度,成功读取事件信息后,便可以根据inotify_event结构体的字段判断事件类型,以及产生事件的文件路径.

//demo 代码
#include 
#include 
#include 
#include 

int watch_inotify_events(int fd) {
    char event_buf[512];
    int ret;
    int event_pos = 0;
    int event_size = 0;
    struct inotify_event *event;
    /*读事件是否发生,没有发生则会阻塞*/
    ret = read(fd, event_buf, sizeof(event_buf));
    /*若read的返回值,小于inotify_event大小,说明我们连最基本的一个event都读取出来,是错误的结果*/
    if(ret < (int)sizeof(struct inotify_event)) {
        printf("read error,could get event");
        return -1;
    }
    /*一次读取可能会去读取多个事件,需要一个循环全部读取出来*/
    while(ret > (int)sizeof(struct inotify_event)) {
        event = (struct inotify_event*)(event_buf + event_pos);
        if(event->len) {
            if(event->mask & IN_CREATE) {
                printf("create file:%s successfully \n", event->name);
            } else {
                printf("delete file:%s successfully \n", event->name);
            }
        }
        //event 的真实大小,name是可变的,所以要加上event->len
        event_size = sizeof(struct inotify_event) + event->len;
        ret -= event_size;
        event_pos += event_size;
    }
    return 0;
}
int main(int argc, char** argv) {
    int inotifyFd;
    int ret;
    if (argc != 2) {
        printf("useage: %s  \n", argv[0]);
    }

    /*inotify初始化*/
    inotifyFd = inotify_init();
    if(inotifyFd == -1) {
        printf("inotify_init error!\n");
        return -1;
    }
    ret = inotify_add_watch(inotifyFd, argv[1], IN_CREATE | IN_DELETE) ;
    watch_inotify_events(inotifyFd);
    if(inotify_rm_watch(inotifyFd, ret) == -1) {
        printf("notify_rm_watch error!\n");
        return -1;
    }
    //关闭描述符
    close(inotifyFd);
    return 0;
}
  • 总结
    整个使用过程如下:

  • 通过inotify_init()创建一个inotify对象

  • 通过inotify_add_watch将一个或多个监听添加进inotify对象中.(IN_CREATEE,IN_DELETE等)

  • 通过read函数从inotify对象中去读取监听事件,当没有新事件发生时间,inotify对象中无任何可读数据

  • 注意点
    虽然inotify机制避免了轮询文件系统的麻烦,但是inotify不是通过回调的方式去通知事件的,而是需要使用者自动去从inotify对象中去读取事件,这就有一个问题发生了,使用者什么时候去主动读取事件呢?
    这个就需要去借助Linux的Epoll了.

2.Epoll机制

  • Epoll可以使用一次等待监听多个描述符的可读/可写状态.等待返回时携带了可读的描述符或者自定义的数据.不需要为每个描述符创建独立的线程进行阻塞读取,避免了资源浪费.
  • Epoll机制有三个函数
  • int epoll_create(int max_fds):创建一个epoll对象的描述符,以供之后去使用.max_fds参数表示可以监听的最大描述符变量.
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event):用于等待事件到来.当此函数返
    回时,events数组参数中将会包含产生事件的文件描述符.
    • epfd: epoll_create()的返回值
    • op: 表示动作,包括三个宏:EPOLL_CTL_ADD:注册新的fd到efpd,EPOLL_CTL_MOD:修改已经注册的fd的监听事件,EPOLL_CTL_DEL:从epfd中删除一个fd
    • fd: 需要监听的文件描述符
    • event: 告诉内核需要监听什么事件.
    typedef union epoll_data {
        void* ptr;
        int fd;
        __unint32_t u32;
        __unint64_t u64;
    } epoll_data_t;
    
    struct epoll_event {
        __unint32_t events;/*事件掩码,指明需要监听的事件种类*/
        epoll_data_t data;/*使用者自定义的数据,当此事发生时,该数据将原封不动地返回给使用者*/
    }
    
    events可以是以下几个宏的集合:
    EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    EPOLLOUT:表示对应的文件描述符可以写;
    EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    EPOLLERR:表示对应的文件描述符发生错误;
    EPOLLHUP:表示对应的文件描述符被挂断;
    EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说    
    的。
    EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,  
    需要再次把这个socket加入到EPOLL队列里
    
  • epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout):
    • epfd:
    • events: 分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不能是空指针,因为内核只负责把数据赋值到这个events数组中,不会去帮助我们在用户态中分配内存).
    • maxevents: 告诉内核events有多大,最大不能大于创建epoll_create()时的size.
    • timeout: 超时时间(ms, 0立即返回,-1将不确定).
    • 返回值: 若函数调用成功,返回对应I/O上已准备好的文件描述符数目,若返回0则表示超时.
    //demo代码
    struct epoll_event eventItem;
    memset(&eventItem, 0, sizeof(eventItem));
    eventItem.events = EPOLLIN | EPOLLOUT | EPOLLERR | EPOLLHUP;//需要监听的事件
    eventItem.data.fd = inotifyId;//需要监听的fd
    int epfd = epoll_create(8);
    int epctl = epoll_ctl(epfd, EPOLL_CTL_ADD, inotifyId, &eventItem);//epoll_ctl可以注册多次,去注册不同需要监听的事件,主要就是不同的文件描述符注册到epoll对象中.注册完之后就可以通过epoll_wait函数等待事件到来.
    struct epoll_event mPendingEventItems[16];
    int result = epoll_wait(epfd, mPendingEventItems, 16, 20*1000);
    
  • 总结,使用步骤如下:
    • 1.使用epoll_create()创建一个epoll对象
    • 2.为需要监听的描述符填充epoll_events结构体,并使用epoll_ctl注册到epoll对象中.
    • 3.使用epoll_wait()等待事件发生
    • 4.根据epoll_wait()返回的epoll_events结构体数组判断事件的类型与来源进行处理
    • 5.继续使用epoll_wait()等待新事件发生.
//完整demo代码
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int watch_inotify_events(int fd) {
    char event_buf[512];
    int ret;
    int event_pos = 0;
    int event_size = 0;
    struct inotify_event *event;
    /*读事件是否发生,没有发生则会阻塞*/
    ret = read(fd, event_buf, sizeof(event_buf));
    /*若read的返回值,小于inotify_event大小,说明我们连最基本的一个event都读取出来,是错误的结果*/
    if(ret < (int)sizeof(struct inotify_event)) {
        printf("read error,could get event");
        return -1;
    }
    /*一次读取可能会去读取多个事件,需要一个循环全部读取出来*/
    while(ret > (int)sizeof(struct inotify_event)) {
        event = (struct inotify_event*)(event_buf + event_pos);
        if(event->len) {
            if(event->mask & IN_CREATE) {
                printf("create file:%s successfully \n", event->name);
            } else {
                printf("delete file:%s successfully \n", event->name);
            }
        }
        //event 的真实大小,name是可变的,所以要加上event->len
        event_size = sizeof(struct inotify_event) + event->len;
        ret -= event_size;
        event_pos += event_size;
    }
    return 0;
}


int main(int argc, char** argv) {
    int epollFd;
    int inotifyFd;
    int pendingEventCount;
    int pendingEventIndex;
    epollFd = epoll_create(8);
    inotifyFd = inotify_init();
    int result_notify = inotify_add_watch(inotifyFd, argv[1], IN_CREATE | IN_DELETE);
    if(result_notify < 0) {
        printf("Could not register INotify. \n");
        return -1;
    }
    if(epollFd < 0) {
        printf("Could not create epoll instance. \n");
        return -1;
    }
    struct epoll_event eventItem;
    eventItem.events = EPOLLIN;
    int result_epoll = epoll_ctl(epollFd, EPOLL_CTL_ADD, inotifyFd, &eventItem);
    if(result_epoll != 0) {
        printf("Could not add INotify to epoll instance.  \n");
    }
    struct epoll_event pendingEventItems[16];
    int pollResult = epoll_wait(epollFd, pendingEventItems, 16, 30*1000ll);
    if(pollResult > 0) {
        pendingEventCount = size_t(pollResult);
    } else {
        pendingEventCount = 0;
    }
    while(pendingEventIndex < pendingEventCount) {
        const struct epoll_event& eventItem = pendingEventItems[pendingEventIndex++];
        if (eventItem.events & EPOLLIN) {
            watch_inotify_events(inotifyFd);
        }
    }
    return 0;
}
123@123-good-man:./a.out ~/Documents/cpptest/ &

//运行结果
123@123-good-man:~/Documents/cpptest$ touch 201808.txt
create file:201808.txt successfully 
[1]+  Done                    ./a.out ~/Documents/cpptest/
123@123-good-man:~/Documents/cpptest$ 

参考:《深入理解Android卷|||》 第五章 深入理解Android输入系统

你可能感兴趣的:(Android Input 学习-INotify与Epoll机制)