Kqueue ooo...

实在不想起一个犯困的标题了,但也没想好要起什么,就这样吧,本篇主要涉及的对象是kqueue。

主要API

EV_SET

EV_SET是一个宏:

1
2
3
4
5
6
7
8
9
#define EV_SET(kevp, a, b, c, d, e, f) do { \
struct kevent *__kevp__ = (kevp); \
__kevp__->ident = (a); \
__kevp__->filter = (b); \
__kevp__->flags = (c); \
__kevp__->fflags = (d); \
__kevp__->data = (e); \
__kevp__->udata = (f); \
} while(0)

kevent结构体

代码如下:

1
2
3
4
5
6
7
8
struct kevent {
uintptr_t ident; /* identifier for this event */
int16_t filter; /* filter for event */
uint16_t flags; /* general flags */
uint32_t fflags; /* filter-specific flags */
intptr_t data; /* filter-specific data */
void *udata; /* opaque user data identifier */
};
  • ident:文件描述符,比如打开一个新的文件,它的文件描述符会是3;
  • filter:过滤器,可被注册监控的事件;
  • flags:传入的filter事件如何设置
    • EV_ADD:添加该事件到kqueue
    • EV_DELETE:从kqueue删除该事件
    • EV_ENABLE:该事件是否可用,默认可用
  • fflags:特定的过滤器flag;
  • data:特定的过滤器数据值;
  • opaque user data identifier;

filter & fflags

filter比较重要,需要单独列出。

filter:

  • #define EVFILT_READ (-1);
  • #define EVFILT_WRITE (-2);
  • #define EVFILT_AIO (-3);
  • #define EVFILT_VNODE (-4):检测文件改动,fflags可以具体到特定事件;

fflags:

  • #define NOTE_DELETE 0x0001:文件删除;
  • #define NOTE_WRITE 0x0002:文件写入;
  • #define NOTE_EXTEND 0x0004:文件扩展;
  • #define NOTE_ATTRIB 0x0008:属性改变;
  • #define NOTE_LINK 0x0010:文件被链接的数量改变;
  • #define NOTE_RENAME 0x0020:文件重命名;
  • #define NOTE_EXIT 0x80000000:文件退出;

kevent()

代码如下:

1
2
3
4
5
6
7
int kevent(
int kq,
const struct kevent *changelist,
int nchanges,
struct kevent *eventlist,
int nevents,
const struct timespec *timeout);
  • kq:kqueue返回值;
  • changelist & nchanges:注册/反注册事件数组;
  • eventlist & nevents:向调用进程返回的事件数组;
  • timeout:功能如其名,设置为NULL时,一直等待;

本人并没有找到kevent函数的源码,一切都封装好,两个数组如何使用,见后面内容。

逻辑解析

kqueue由于高度封装,模仿示例(man)就能写出代码。下面先按照本人的理解整理一下基本的逻辑思路,这里以监控文件改动为例(为了方便理解采用了面向对象的角度):

创建kqueue对象kq(由kqueue()返回)、创建一个kevent结构体ev,作用是收集参数、创建用于监控的打开文件fd。

使用宏EV_SET,把ev参数初始化,那么就需要对这些参数有了解:

  • 第一个参数就是监控打开文件fd;
  • 第二个参数filter就是事件;
  • 如何处理filter事件?是设定入kq,还是从kq中删除?这就引入了第三个参数flag,常用的包括EV_ADD、EV_ENABLE等等;
  • 有些事件需要更细的设定,比如监控改动这个事件EVFILT_VNODE,改动的方式有很多,删除(NOTE_DELETE)、写入、属性改变等等,那么也应该将“关注”的这些动作进行设定声明,这些设定需要传给第四个参数;
  • 第五个参数data用于返回一些特定数据;
  • 第六个udata用于携带一些信息,可以在一些情形发挥作用;

这个饱含信息量的结构体最后会进入kevent中和kq发生化学反应,什么反应就需要知道源码,前面说了我不知道,但是通常也不需要知道,只需要明白这个函数的效果就足够。

一开始看代码的时候没有弄清楚changelist和eventlist的用处,后来发现并不复杂,前者用来注册事件,后者用来实际监控注册的事件。

比如先初始化一个结构体数据:

1
2
3
4
5
6
7
EV_SET(&ev,
fd,
EVFILT_VNODE,
EV_ADD | EV_ENABLE,
NOTE_DELETE|NOTE_WRITE,
0,
0)

注册就可以通过下面代码完成:

1
kevent(kq, &ev, 1, NULL, 0, {1, 0})

而监控的时候可以使用代码:

1
2
struct kevent eve[1];
kevent(kq, NULL, 0, eve, 1, {1, 0});

一开始本人困惑于eve如何初始化,不过很快就会发现,kevent函数完成了一切工作,这里eve就是之前注册的ev,在kevent()内部应该完成了一次数据的传递。

总之很奇怪的写法,但又不得不接受,这么写可能有一些基于性能的考虑,也可能,就是早起代码不规范的产物,又或者是为了隐匿具体实现,但是只要能用,就是好的。

实例

写一个最简单的实例来进行说明,这个例子主要是监控本地磁盘上readme.md文件的写事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main(int argc, char *argv[]) {
int fd, kq, n;
struct kevent rege[1];
struct kevent e[1];
fd = open(argv[1], O_RDWR);
kq = kqueue();
EV_SET(rege, fd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, NOTE_WRITE, 0, 0);
for (;;) {
n = kevent(kq, rege, 1, e, 1, NULL);
if (n == -1)
err(1, "出现错误!");
// if (nev == 0)
// continue;
if (e->fflags & NOTE_WRITE) {
printf("出现了写事件!");
e->fflags &= ~NOTE_WRITE;
}
printf("\n");
if (e->fflags) {
warnx("无法解析的事件!");
}
}
}

当对readme.md进行写操作的时候,代码可以对该事件进行捕捉。

如果设置了timeout,那么不能取消注释。

小结

IO多路复用是Java NIO的基础,kqueue通过kevent函数对底层的实现进行了很好的封装,调用就完事。

参考