了解kqueue

74 阅读5分钟

1.2.2 kqueue

简介

kqueue是BSD中使用的内核事件通知机制。提供了一种用于一个或者多个进程同步的简单而高效的方法。

其中一个kqueue代表一个描述符,这个描述符会阻塞等待一个特定类型或者种类的事件发生,用户态的进程可以等待这个描述符。

kqueue和对应的kevent构成了内核异步I/O的基础,kevent代表事件的数据结构。其结构如下:

struct kevent {
	uintptr_t       ident; //与事件相关的描述符,比如文件描述符或目录描述符
	int16_t         filter;//表示触发事件的过滤器类型,通常为EVFILT_VNODE,用于文件系统事件。
	uint16_t        flags;//标志位,指示发生的事件类型和状态,比如文件的创建、删除、写入等
	uint32_t        fflags;//更详细的事件标志,提供了更多关于发生事件类型的信息,比如文件权限、文件大小等
	intptr_t        data;//可能包含一些额外的数据,具体含义取决于事件类型和标志。
	void            *udata;//用户数据,通常用于与特定事件关联的自定义数据。
};

如何使用kqueue?

我们可以通过以下步骤使用kqueue

  1. 创建kqueue对象:使用系统函数kqueue()创建一个'kqueue'对象。这个对象是用来存储事件和对事件进行操作的
  2. 注册事件: 使用EV_SET宏定义设置指定的事件,即初始化kevent结构体。在调用kevent或者kevent64注册事件
  3. 监听事件: 在循环中使用kevent()系统调用监听事件,kevent将会阻塞程序,直到有事件发生或超时。
  4. 处理事件:一旦事件被检测到,可以根据事件类型执行相应的操作

以下是一个简单的示例,展示了如何在 macOS 中使用 kqueue 监控文件系统事件(如文件的读取、写入、删除):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>

int main() {
    int kq, i;
    struct kevent events;
    struct timespec timeout = { 1, 0 }; // 设置超时时间为1秒

    // 创建 kqueue
    kq = kqueue();
    if (kq == -1) {
        perror("kqueue");
        exit(EXIT_FAILURE);
    }

    // 打开文件
    const char *file_path = "/path/to/your/file"; // 修改为你要监控的文件路径
    int file_fd = open(file_path, O_RDONLY);
    if (file_fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 注册事件
    EV_SET(&events, file_fd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR, 
           NOTE_WRITE | NOTE_DELETE, 0, NULL);
    if (kevent(kq, &events, 1, NULL, 0, NULL) == -1) {
        perror("kevent");
        exit(EXIT_FAILURE);
    }

    // 监听事件
    while (1) {
        i = kevent(kq, NULL, 0, &events, 1, &timeout);
        if (i == -1) {
            perror("kevent");
            exit(EXIT_FAILURE);
        } else if (i > 0) {
            if (events.flags & EV_ERROR) {
                fprintf(stderr, "Error in event\n");
                continue;
            }
            if (events.fflags & NOTE_WRITE) {
                printf("File was written\n");
            }
            if (events.fflags & NOTE_DELETE) {
                printf("File was deleted\n");
            }
        }
    }

    close(kq);
    return 0;
}

在设置指定事件的时候,可以设置一些事件过滤器,如果满足过滤器的话则返回,否则阻塞。系统支持一些“预定义”的过滤器,如下表所示:

事件过滤器常量用途
EVFILT_READ-1如果监视文件,那么当文件指针没有在EOF时返回。如果监视套接字、管道或者FIFO,那么当有数据可读时就返回
EVFILT_WRITE-2如果监视文件,那么当文件可以写入时返回 如果监控套接字、管道或FIFO,那么当数据可以写入时返回,在事件数据中返回的缓冲区的可用空间
EVFILT_AIO-3监视一个或多个异步 I/O 请求的状态变化。异步 I/O 操作允许应用程序发出读取或写入请求,并在后台进行,当完成时会发送通知,而不会阻塞主线程。
EVFILT_VNODE-4文件(vnode)相关系统调用的过滤器,例如rename,delete,unlink,link
EVFILT_PROC-5监视一个指定PID表示的进程调用execute、exit、fork、wait等,但是这个flag过期了
EVFILT_SIGNAL-6监视发给一个进程的特性信号,即使这个信号被进程忽略(kill -9 SIGKILL监控不了)
EVFILT_TIMER-7最高能达到纳秒精确度周期定时器
EVFILT_MACHPORT-8监视一个Mach port或一个port组,如果监视的port接收到一条消息,则返回
EVFILT_FS-9监视指定路径下的文件系统事件。它允许你捕获文件系统级别的变化,而不需要轮询文件系统状态。常见的事件:NOTE_WRITE,NOTE_DELETE,NOTE_RENAME,NOTE_WRITENOTE_ATTRIB
EVFILT_VM-12虚拟内存相关的通知,用于处理内存压力
EVFILT_EXCEPT-15监视文件描述符上发生的异常情况,如带外数据的到达(NOTE_OOB)、套接字错误(NOTE_SOCKERR)等。

kqueue的优点

  1. 高效性:kqueue 允许在单个系统调用中管理大量的事件,处理大规模事件时效率高,避免了传统轮询机制的性能瓶颈。

  2. 多样化事件支持: 支持多种事件类型的监控,可以监视文件描述符、套接字、信号、定时器等多种事件,提供了灵活的事件监控机制。

  3. 可扩展性: 对于大量事件和高并发场景表现出良好的可扩展性,能够轻松处理大量的事件。

  4. 跨平台支持: 虽然最初是在 BSD 系统中引入,但类似的事件通知机制也在其他类 Unix 系统中实现,因此具有跨平台的优势。