Android_INotify与Epoll

1.INotify介绍与使用

INotify是一个Linux内核所提供的一种文件系统变化通知机制。它可以为应用程序监控文件系统的变化,如文件的新建、删除、读写等。INotify机制有两个基本对象,分别为inotify对象与watch对象,都使用文件描述符表示。

inotify对象对应了一个队列,应用程序可以向inotify对象添加多个监听。当被监听的事件发生时,可以通过read()函数从inotify对象中将事件信息读取出来。Inotify对象可以通过以下方式创建:

1
int inotifyFd = inotify_init();

而watch对象则用来描述文件系统的变化事件的监听。它是一个二元组,包括监听目标和事件掩码两个元素。监听目标是文件系统的一个路径,可以是文件也可以是文件夹。而事件掩码则表示了需要需要监听的事件类型,掩码中的每一位代表一种事件。可以监听的事件种类很多,其中就包括文件的创建(IN_CREATE)与删除(IN_DELETE)。
读者可以参阅相关资料以了解其他可监听的事件种类。以下代码即可将一个用于监听输入设备节点的创建与删除的watch对象添加到inotify对象中:

1
int wd = inotify_add_watch (inotifyFd, “/dev/input”,IN_CREATE | IN_DELETE);

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

1
2
3
4
5
6
7
struct inotify_event {
__s32 wd; /* 事件对应的Watch对象的描述符 */
__u32 mask; /* 事件类型,例如文件被删除,此处值为IN_DELETE */
__u32 cookie;
__u32 len; /* name字段的长度 */
char name[0]; /* 可变长的字段,用于存储产生此事件的文件路径*/
};

当没有监听事件发生时,可以通过如下方式将一个或多个未读取的事件信息读取出来:

1
size_t len = read (inotifyFd, events_buf,BUF_LEN);

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

总结一下INotify机制的使用过程:

  • 通过inotify_init()创建一个inotify对象。
  • 通过inotify_add_watch将一个或多个监听添加到inotify对象中。
  • 通过read()函数从inotify对象中读取监听事件。当没有新事件发生时,inotify对象中无任何可读数据。

通过INotify机制避免了轮询文件系统的麻烦,但是还有一个问题,INotify机制并不是通过回调的方式通知事件,而需要使用者主动从inotify对象中进行事件读取。

那么何时才是读取的最佳时机呢?这就需要借助Linux的另一个优秀的机制Epoll了。

2.Epoll介绍与使用

无论是从设备节点中获取原始输入事件还是从inotify对象中读取文件系统事件,都面临一个问题,就是这些事件都是偶发的。也就是说,大部分情况下设备节点、inotify对象这些文件描述符中都是无数据可读的,同时又希望有事件到来时可以尽快地对事件作出反应。

为解决这个问题,我们不希望不断地轮询这些描述符,也不希望为每个描述符创建一个单独的线程进行阻塞时的读取,因为这都将会导致资源的极大浪费。

此时最佳的办法是使用Epoll机制。

Epoll可以使用一次等待监听多个描述符的可读/可写状态。等待返回时携带了可读的描述符或自定义的数据,使用者可以据此读取所需的数据后可以再次进入等待。

因此不需要为每个描述符创建独立的线程进行阻塞读取,避免了资源浪费的同时又可以获得较快的响应速度。

Epoll机制的接口只有三个函数,十分简单。

  • epoll_create(int max_fds):创建一个epoll对象的描述符,之后对epoll的操作均使用这个描述符完成。max_fds参数表示了此epoll对象可以监听的描述符的最大数量。
  • epoll_ctl (int epfd, int op,int fd, struct epoll_event *event):用于管理注册事件的函数。这个函数可以增加/删除/修改事件的注册。
  • int epoll_wait(int epfd, structepoll_event * events, int maxevents, int timeout):用于等待事件的到来。当此函数返回时,events数组参数中将会包含产生事件的文件描述符。

接下来以监控若干描述符可读事件为例介绍一下epoll的用法。

(1) 创建epoll对象

首先通过epoll_create()函数创建一个epoll对象:

1
Int epfd = epoll_create(MAX_FDS)

(2) 填充epoll_event结构体

接着为每一个需监控的描述符填充epoll_event结构体,以描述监控事件,并通过epoll_ctl()函数将此描述符与epoll_event结构体注册进epoll对象。

epoll_event结构体的定义如下:

1
2
3
4
struct epoll_event {
__uint32_tevents; /* 事件掩码,指明了需要监听的事件种类*/
epoll_data_t data; /* 使用者自定义的数据,当此事件发生时该数据将原封不动地返回给使用者 */
};

epoll_data_t联合体的定义如下,当然,同一时间使用者只能使用一个字段:

1
2
3
4
5
6
typedef union epoll_data {
void*ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;

epoll_event结构中的events字段是一个事件掩码,用以指明需要监听的事件种类,同INotify一样,掩码的每一位代表了一种事件。

常用的事件有EPOLLIN(可读),EPOLLOUT(可写),EPOLLERR(描述符发生错误),EPOLLHUP(描述符被挂起)等。更多支持的事件读者可参考相关资料。

data字段是一个联合体,它让使用者可以将一些自定义数据加入到事件通知中,当此事件发生时,用户设置的data字段将会返回给使用者。

在实际使用中常设置epoll_event.data.fd为需要监听的文件描述符,事件发生时便可以根据epoll_event.data.fd得知引发事件的描述符。

当然也可以设置epoll_event.data.fd为其他便于识别的数据。

填充epoll_event的方法如下:

1
2
3
4
 structepoll_event eventItem;
memset(&eventItem, 0, sizeof(eventItem));
eventItem.events = EPOLLIN | EPOLLERR | EPOLLHUP; // 监听描述符可读以及出错的事件
eventItem.data.fd= listeningFd; // 填写自定义数据为需要监听的描述符

接下来就可以使用epoll_ctl()将事件注册进epoll对象了。

epoll_ctl()的参数有四个:

  • epfd是由epoll_create()函数所创建的epoll对象的描述符。
  • op表示了何种操作,包括EPOLL_CTL_ADD/DEL/MOD三种,分别表示增加/删除/修改注册事件。
  • fd表示了需要监听的描述符。
  • event参数是描述了监听事件的详细信息的epoll_event结构体。

注册方法如下:

1
2
// 将事件监听添加到epoll对象中去
result =epoll_ctl(epfd, EPOLL_CTL_ADD, listeningFd, &eventItem);

重复这个步骤可以将多个文件描述符的多种事件监听注册到epoll对象中。

完成了监听的注册之后,便可以通过epoll_wait()函数等待事件的到来了。

(3) 使用epoll_wait()函数等待事件

epoll_wait()函数将会使调用者陷入等待状态,直到其注册的事件之一发生之后才会返回,并且携带了刚刚发生的事件的详细信息。其签名如下:

1
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • epfd是由epoll_create()函数所创建的epoll对象描述符。
  • events是一个epoll_event的数组,此函数返回时,事件的信息将被填充至此。
  • maxevents表示此次调用最多可以获取多少个事件,当然,events参数必须能够足够容纳这么多事件。
  • timeout表示等待超时的事件。epoll_wait()函数返回值表示获取了多少个事件。
  • 处理事件
    epoll_wait返回后,便可以根据events数组中所保存的所有epoll_event结构体的events字段与data字段识别事件的类型与来源。
    Epoll的使用步骤总结如下:
  • 通过epoll_create()创建一个epoll对象。
  • 为需要监听的描述符填充epoll_events结构体,并使用epoll_ctl()注册到epoll对象中。
  • 使用epoll_wait()等待事件的发生。
  • 根据epoll_wait()返回的epoll_events结构体数组判断事件的类型与来源并进行处理。
  • 继续使用epoll_wait()等待新事件的发生。

3.INotify与Epoll的小结

INotify与Epoll这两套由Linux提供的事件监听机制以最小的开销解决了文件系统变化以及文件描述符可读可写状态变化的监听问题。

它们是Reader子系统运行的基石,了解了这两个机制的使用方法之后便为对Reader子系统的分析学习铺平了道路。