文件描述符
文件描述符是一个句柄,用来表示一个文件,使用文件描述符(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机制的使用过程如下:
- 通过inotify_init()创建一个inotify对象
- 通过inotify_add_watch将一个或多个监听添加进inotify对象中.(IN_CREATEE,IN_DELETE等)
- 通过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机制的使用过程如下:
- 通过epoll_create()创建一个epoll对象
- 为需要监听的描述符构建epoll_events结构体,使用epoll_ctl()注册到epoll对象中
- 使用epoll_wait()等待事件发生
- 根据epoll_wait()返回的epoll_event结构体数组判断事件的类型与来源并进行处理
- 继续使用epoll_wait()等待新事件发生
参考:
- Linux文件描述符到底是什么?
- 彻底搞懂epoll高效运行的原理
- 《深入理解Android卷3》 第5章 5.2.1 P186