请稍侯

macOS/iOS中的I/O多路复用方案kqueue

12 March 2021

macOS/iOS中的I/O多路复用方案kqueue

XNU内核

Mach+BSD内核

尽管XUN的绝对核心是Mach,但是XUN向用户态提供的主要接口是BSD接口。BSD层是建立在Mach之上,是XNU中一个不可分割的一部分。BSD负责提供可靠的、现代的API。其内容包括:

UNIX进程模型
POSIX线程模型及相关的同步原语
UNIX用户和组
网络协议栈
文件访问系统
设备访问

kqueue是BSD中使用的内核事件通知机制,一个kqueue指的是一个描述符,这个描述符会阻塞等待直到一个特定的类型和种类的事件发生。用户态或者内核的进程可以等待这个描述符,因而kqueue提供了一种用于一个或多个进程同步的简单且高效的方法。kqueue和kevent(表示事件的数据结构)构成了内核异步I/O的基础。 kqueue 是 FreeBSD 上的一种的多路复用机制。它是针对传统的 select/poll 处理大量的文件描述符性能较低效而开发出来的。注册一堆描述符到 kqueue 以后,当其中的描述符状态发生变化时, kqueue 将一次性通知应用程序哪些描述符可读、可写或出错了。 kqueue 支持多种类型的文件描述符,包括 socket 、信号、定时器、 AIO 、 VNODE 、 PIPE; kqueue 对 AIO , POSIX 的异步 IO系列的支持,是异步行为完成通知机制之一。

常见的io复用有select、poll、epoll、kqueue等。其中epoll为Linux系统独有,kqueue则在众多unix系统中存在。在go语言中kqueue相关代码位于syscall库中。

  • event_tKevent()操作的最基本的事件结构。
  • kqueuefreebsd内核中的一个事件队列kernel queue
  • Kevent()是一个系统调用,也是kqueue的用户界面,是对kqueue进行添加,删除操作的用户态的界面。
type Kevent_t struct {
	Ident  uint64	//该事件关联的描述符,常见的有socket fd,file fd, signal fd等
	Filter int16	//事件的类型,比如读事件EVFILT_READ,写事件EVFILT_WRITE,信号事件EVFILT_SIGNAL
	Flags  uint16	//事件的行为,也就是对kqueue的操作,下面介绍几个常用的
					//如EV_ADD:添加到kqueue中,EV_DELETE从kqueue中删除
					//EV_ONESHOT:一次性或事件,kevent返回后从kqueue中删除
					//EV_CLEAR:事件通知给用户后,事件的状态会重置,
	Fflags uint32
	Data   int64
	Udata  *byte	//用户指定的数据
}
func Kevent(kq int, 				//Kqueue返回的唯一参数值,标记着一个内核队列
			changes, 				//需要对kqueue进行修改的事件集合,kqueue通过此参数完成对事件的修改
			events []Kevent_t, 		//返回的已经就绪的事件列表
			timeout *Timespec) 		//超时控制,不指定事件表示一直等待事件发生,否则只等待一段时间
			(n int, err error) {	//返回已经就绪的事件数量
...
...
}

三种现在高效的I/O模型

最常见的I/O复用模型 select select先阻塞,有活动套接字才返回。与阻塞I/O相比,select会有两次系统调用,但是select能处理多个套接字。

信号驱动 signal driven I/O (SIGIO) 只有UNIX系统支持,与I/O multiplexing (select and poll)相比,它的优势是,免去了select的阻塞与轮询,当有活跃套接字时,由注册的handler处理。

完全异步的I/O asynchronous I/O (the POSIX aio_functions) 很少有*nix系统支持,windows的IOCP则是此模型,完全异步的I/O复用机制,因为纵观上面其它四种模型,至少都会在由kernel copy data to appliction时阻塞。而该模型是当copy完成后才通知application,可见是纯异步的。好像只有windows的完成端口是这个模型,效率也很出色。

几种模型的比较 从图中可以看出,越往后,阻塞越少,理论上效率也是最优。

epoll,kqueue比select高级,主要是因为他们无轮询,因为他们用callback取代了。 当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

  • 只有IOCP是asynchronous I/O,其他机制或多或少都会有一点阻塞。
  • select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善
  • epoll, kqueue是Reacor模式,IOCP是Proactor模式。
  • java nio包是select模型。

参考: IO复用 – kqueue
kQueue目录监控 (kQueue directory monitoring)