深入分析Kqueue & Kevent

为什么用Kqueue?

Wiki上的解释:

kqueue 是一种可扩展的事件通知接口。2000 年 7 月发布的 FreeBSD 4.1 中首次引入了 kqueue,随后也被 NetBSD、OpenBSD、macOS 等操作系统支持。
kqueue 在内核与用户空间之间充当输入输出事件的管线。因此在事件循环的迭代中,进行一次 kevent(2) 系统调用不仅可以接收未决事件,还可以修改事件过滤器。

简单解释,Kqueue是unix系统上高效的IO多路复用技术(常见的io复用有select、poll、epoll、kqueue等等,其中epoll为Linux系统独有,kqueue则在众多unix系统中存在)。

  • 为什么要有IO多路复用?
    阻塞I/O模式下,一个线程只能处理一个流的I/O事件(比如用户线程发起一个IO请求操作,内核会去查看要读取的数据是否就绪,如果数据没有就绪,则会一直在那等待,直到数据就绪,当数据就绪之后,便将数据拷贝到用户线程)。所以,如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。

所以,考虑非阻塞忙轮询的I/O方式,我们发现我们可以同时处理多个流了。使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。select/poll是通过轮询的方法来获得就绪的状态,调用select/poll后就阻塞住,直到有就绪的文件描述符,或者超时,或者被中断。返回值是就绪的文件描述符的个数,需要遍历作为参数传入的文件描述符的位域或数组获得文件描述符。

Kqueue和Epoll的优势:通过callback避免忙轮询

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。
kqueue与epoll非常相似,在注册一批文件描述符到 kqueue 以后,当其中的描述符状态发生变化时,kqueue将一次性通知应用程序哪些描述符可读、可写或出错了(即产生事件Event)。

什么是Kqueue?

kqueue是freebsd内核中的一个事件队列kernel queue。在kqueue实现中,比较关键的是一个knote结构体,该结构体在内核空间对应于应用层的kevent结构体。

kqueue的实现由三个子结构体组成,每个结构体的基本元素是knote:

  • 一个队列,用来保存active的knotes节点。(有事件发生的节点,这个队列就是已完成事件队列)
  • 一个hashtable 用来存储和查找identity->descriptor的映射。
  • 线性的描述符list,用来存储和查找有对应描述符的knotes节点。(用于保证当事件kevent的fd文件描述符被关闭后,对应的knote被释放)

在一个 kqueue 中,{ident, filter} 确定一个唯一的事件,这个事件被称为Kevent,它的结构体如下:

type Kevent_t struct {
   Ident  uint64 
   Filter int16 
   Flags  uint16
   Fflags uint32
   Data   int64
   Udata  *byte
}
  • Ident:事件的 id,一般设置为文件描述符。在 socket 使用中,它是 socket 的 fd 句柄。
  • Filter: filter 是事件的类型,有 15 种。内核检测 ident 上注册的 filter 的状态,状态发生了变化,就通知应用程序。
    kevent 定义了较多的 filter,比如与socket读写相关的filter:
    • EVFILT_READ:TCP 监听 socket,如果在完成的连接队列 ( 已收三次握手最后一个 ACK) 中有数据,此事件将被通知。收到该通知的应用一般调用 accept(),且可通过 data 获得完成队列的节点个数。 流或数据报 socket,当协议栈的 socket 层接收缓冲区有数据时,该事件会被通知,并且 data 被设置成可读数据的字节数。
    • EVFILT_WRIT:当 socket 层的写入缓冲区可写入时,该事件将被通知;data 指示目前缓冲区有多少字节空闲空间。
    • EVFILT_USER 用户自定义的事件,由用户代码触发(而非内核触发)
  • Flags:操作事件的方式,比如,EV_ADD 添加事件到Kqueue,EV_DELETE 删除,EV_ENABLE 激活,EV_DISABLE 不激活。
  • FFlags:Filter-specific flags,特定 filter 的专有标志,可用于保存专有返回信息。
  • Data:int 型的用户数据,特定 filter 存储专有信息,比如socket 里面它是可读写的数据长度。
  • UData:指针类型的数据,你可以携带任何想携带的附加数据。比如对象、指针地址(Opaque User Data Identifier)。

Kqueue怎么运行?

Kqueue是这样初始化的:

  • 获取和分配Kqueue
    最初, 应用程序调用kqueue() 来分配一个新的kqueue, 涉及到了分配一个新的kqueue描述符、kqueue结构体、和一个指向已打开文件描述符table的指针, 这个时候并没有给这个给array和hashtable分配空间.
  • 链接Kevent和Knote
    应用调用kevent()传递一个changelist指针(Kevents的数组),changelist中的kevents从用户空间copy到内核空间, 然后对每一个kevents调用register():register()先在KQ中查找是否有匹配的knotes, 如果过没有,表明第一次添加,分配一个新的knotes(有EV_ADD标记).根据传递来的kevent信息对新建的knotes进行初始化,并调用attacth()将knote连接到事件源(如tcp收包)。
  • 添加Knote到队列
    之后将knote添加到kqueue的hashtable或array中

Kqueue是这样运行的:

  1. 事件源有事件发生时,即收到事件时,事件源会对attach到自己的knotes链表调用knote()函数:扫描所有link到该事件源的knotes,检测事件是否满足通知条件(KeventFilter), 如果事件条件满足则将该knote放入到kqueue的active list队列里,最终会传递给应用层。

事件是由 <file,filter> 两者唯一确定的。这说明,如果在创建event 时,相同的文件描述符,不同的filter 的组合,最终的创建的事件是不同的。在内核中,相同文件的不同事件由klist 进行管理。当该数据结构上发生某种变化时,就会遍历klist,确定是否有事件发生。

一个示例

使用Kqueue和KEvent非常简单:

func initKqueue() {
   kqueueFd, err := syscall.Kqueue()
   fd := int(os.Stdin.Fd())

   //build kEvent
   //fileFd = 0
   kEvent1 := syscall.Kevent_t{
      Ident:  uint64(fd),
      Filter: syscall.EVFILT_READ,
      Flags:  syscall.EV_ADD,
   }

   //register kEvent to kernel
   _, err = syscall.Kevent(kqueueFd, []syscall.Kevent_t{kEvent1}, nil, nil)
   if err != nil {
      logs.Fatal("register kEvent error, err:%v", err)
   }
   for {
      kEventArr := make([]syscall.Kevent_t, 10)
      keventNum, err := syscall.Kevent(kqueueFd, nil, kEventArr, nil)
      if err != nil {
         logs.Fatal("get kEvent error, err:%v", err)
      }
      logs.Info("kEvent Num:%v", keventNum)
      for i := 0; i < keventNum; i++ {
         logs.Info("kevent:%+v", kEventArr[i])
      }
      time.Sleep(2 * time.Second)
      logs.Info("done.")
   }
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值