Linux的文件描述符 及Inotify、Epoll机制

2,930 阅读6分钟

文件描述符

文件描述符是一个句柄,用来表示一个文件,使用文件描述符(file descriptor)可以来访问文件。当然文件描述符并不是简单的文件的指针,而是一个表的索引值,所以是非负整数,具体关系关系如下图:

文件描述符表每个进程会有一个,“打开文件表”和“i-node表”一个系统只有一个。

文件描述符是“文件描述符表”上的索引值,索引的内容又会指向“打开文件表”,这个表格里的i-node指针会指向i-node表的一个inode,inode包含文件的元信息,linux使用inode来识别文件。

文件描述符

INotify机制

INotify是Linux内核所提供的一种文件系统变化通知机制,相对应的就是轮询机制(select),需要通过不断循环来判断文件系统的变化。

INotify可以为应用程序监控文件系统的变化,如文件的新建、删除、读写等。INotify机制有两个基本对象,分别为inotify对象和watch对象,都使用文件描述符来表示。

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

int inotifyFd = inotify_init();

而watch对象则用来描述文件系统的变化事件的监听。它是一个二元组,包括监听目标和事件掩码两个元素。

  • 监听目标:是文件系统的路径,可以是文件也可以是文件夹。
  • 事件掩码:表示了需要监听的事件类型,包括文件的创建(IN_CREATE)与删除 (IN_DELETE)。读者可以参阅相关资料了解其他可监听的事件种类。

以下代码即可将一个watch对象(用于监听输入设备节点的创建与删除)添加到inotify对象中:

int wd = inotify_add_watch(inotifyfd, "/dev/input",  IN_CREATE | IN_DELETE);

完成上述watch对象的添加后,当/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,也就是说是可变长的,用于存储产生此事件的文件路径*/
};

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

size_len = read (inotifyFd, events_buf, BUF_LEN)

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

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

  1. 通过inotify_init()创建一个inotify对象
  2. 通过inotify_add_watch将一个或多个监听添加进inotify对象中.(IN_CREATEE,IN_DELETE等)
  3. 通过read函数从inotify对象中去读取监听事件,当没有新事件发生时间,inotify对象中无任何可读数据

通过notify机制避免了轮询文件系统的麻烦,但是还有一个问题,inotify并不是通过回调的方式去通知事件,而需要使用者主动从inotify对象中进行事件读取,那么何时才是读取的最佳时机呢?这就需要借助Linux的另一个优秀的机制 Epoll了。

Epoll机制

Epoll是一种I/O事件通知机制,是linux内核I/O多路复用的一个实现。

所谓I/O多路复用,是指在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。这里说的输入输出源可以是文件(file), 网络(socket),进程之间的管道(pipe)。但是在linux系统中,都用文件描述符(fd)来表示。

因此可以说"Epoll可以使用一次等待监听多个描述符的可读/可写状态"。等待返回时携带了可读/可写的描述符或者自定义的数据。不需要为每个描述符创建独立的线程进行阻塞读取,避免了资源浪费。

可读:当文件描述符关联的内核缓冲区非空,有数据可以读取
可写:当文件描述符关联的内核缓冲区不满,有空闲空间可以写入

Epoll机制的接口主要有三个函数:epoll_create、epoll_ctl和epoll_wait

epoll_create
//创建一个epoll对象的描述符epfd,之后对epoll的操作均使用这个描述符完成。用完epoll后,需要调用close()关闭。
//max_fds参数表示可以监听描述符的最大数量。
int epoll_create(int max_fds)
epoll_ctl
//用于管理注册事件的函数。这个函数可以增加、修改、删除事件的注册
//注册后再epoll对象内部使用红黑树来进行管理
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event)
  • epfd: epoll_create()的返回值
  • op: 有三个操作:
    • EPOLL_CTL_ADD为注册新的fd,
    • EPOLL_CTL_MOD为修改已经注册的fd,
    • EPOLL_CTL_DEL为删除一个fd
  • fd: 需要监听的文件描述符
  • event: 告诉内核需要监听什么事件,数据结构如下:
struct epoll_event {
    __unint32_t events;//事件掩码,指明需要监听的事件种类
    epoll_data_t data;//使用者自定义的数据,当此事发生时,该数据将原封不动地返回给使用者
}

typedef union epoll_data {
    void* ptr;
    int fd; //文件描述符
    __unint32_t u32;
    __unint64_t u64;
} epoll_data_t;

epoll_event.data.fd 和 epoll_ctl传入的fd参数是一个值

epoll_wait
//阻塞等待注册的事件发生,返回事件的数目,并将触发的事件写入events数组中
int epoll_wait(int epfd, struct epoll_event* event, int maxevents, int timeout)
  • epfd: epoll_create()的返回值
  • event: 是epoll_event数组,此函数返回时,事件信息被填充至此
  • maxevents最多可以获得多少事件,当然event必须要足够容纳这么多事件
  • timeout表示超时的时间

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

  1. 通过epoll_create()创建一个epoll对象
  2. 为需要监听的描述符构建epoll_events结构体,使用epoll_ctl()注册到epoll对象中
  3. 使用epoll_wait()等待事件发生
  4. 根据epoll_wait()返回的epoll_event结构体数组判断事件的类型与来源并进行处理
  5. 继续使用epoll_wait()等待新事件发生

参考: