0%

epoll、kqueue、iocp

随着Nodejs的流行,事件驱动模型被越来越多的人所熟知,它还有一个更加有名的名字——I/O多路复用,不用的操作系统都有其各自的实现方式。

一、epoll

  1. epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。

epoll的核心是3个API,核心数据结构是:1个红黑树和1个链表。

epoll

  1. 三个步骤

    • 首先,需要调用epoll_create来创建一个epoll的文件描述符,内核会同时创建一个eventpoll的数据结构。这个数据结构里面会包含两个东西,一个是红黑树,专门用于存储epoll_ctl注册进来的fd文件描述符;另外一个是就绪链表,用来存储epoll_wait调用相关的,已经就绪的那些fd文件描述符。
    • 其次,因为epoll中的所有事件,都与网卡驱动程序建立回调关系,当相应的事件发生的时候,会通过这个回调函数,将发生的事件添加到就绪链表当中。
    • 最后,当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有需要处理的事件。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
  2. epoll是通过后台中断的方式来获得就绪的状态,调用epoll_create创建实例,调用epoll_ctl添加或删除监控的文件描述符,调用epoll_wait阻塞住,直到有就绪的文件描述符,通过epoll_event参数返回就绪状态的文件描述符和事件。

    • epoll_create 创建一个epoll对象,一般epollfd = epoll_create()
    • epoll_ctl(epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件
      • epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回
      • epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//缓冲区可写入时epoll_wait返回
    • epoll_wait(epollfd,…)等待直到注册的事件发生
  3. 两种触发方式

    • 边缘触发(edge trigger,ET)
      • 对于读操作
        • 当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候
        • 当有新数据到达时,即缓冲区中的待读数据变多的时候
        • 当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD修改EPOLLIN事件时
      • 对于写操作
        • 当缓冲区由不可写变为可写时
        • 当有旧数据被发送走,即缓冲区中的内容变少的时候
        • 当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD修改EPOLLOUT事件时。
    • 水平触发(level trigger,LT),默认
      • 对于读操作,只要缓冲内容不为空,LT模式返回读就绪。
      • 对于写操作,只要缓冲区还不满,LT模式会返回写就绪。

二、kqueue

  1. kqueue是FreeBSD上的一种的多路复用机制,它是针对传统的select/poll处理大量的文件描述符性能较低效而开发出来的。kqueue与epoll非常相似,最初是2000年Jonathan Lemon在FreeBSD系统上开发的一个高性能的事件通知接口。

  2. 原理:注册一批socket描述符到kqueue以后,当其中的描述符状态发生变化时,kqueue将一次性通知应用程序哪些描述符可读、可写或出错了。

  3. kqueue支持多种类型的文件描述符,包括socket、信号、定时器、AIO、VNODE、PIPE。

  4. kqueue的接口包括kqueue()、kevent()两个系统调用和一个struct kevent结构:

    • kqueue()生成一个内核事件队列,返回该队列的文件描述符,其它 API 通过该描述符操作这个kqueue,如:注册,反注册,获取通知等。
    • kevent()提供向内核注册/反注册事件和返回就绪事件或错误事件。
      • 注册/撤销:kevent()中的neventlist参数将其设为0且传入合法的changelist和nchangelist,就会将changelist中的事件注册到kqueue中。当关闭某文件描述符时,与之关联的事件会被自动地从kqueue移除。
      • 启用禁用/禁止过滤器事件:通过flags EV_ENABLE和EV_DISABLE使过滤器事件有效或无效。这个功能在利用 EVFILT_WRITE 发送数据时非常有用。
      • 获取已触发事件:将nchangelist设置成0及其他参数,当kevent非错误和超时返回时,在eventlist和neventlist中就保存可用事件集合。
    • struct kevent就是kevent()操作的最基本的事件结构:
1
2
3
4
5
6
7
8
struct kevent { 
uintptr_t ident; /* 事件 ID */
short filter; /* 事件过滤器 */
u_short flags; /* 行为标识 */
u_int fflags; /* 过滤器标识值 */
intptr_t data; /* 过滤器数据 */
void *udata; /* 应用透传数据 */
};

参数说明

  • ident:事件的id,一般设置为文件描述符。
  • filter:可以将kqueue filter看作事件。内核检测ident上注册的filter的状态,状态发生了变化,就通知应用程序。

    在一个kqueue中,{ident, filter}确定一个唯一的事件

  • 行为标志flags:
    • EV_ADD:指示加入事件到 kqueue
    • EV_DELETE:指示将传入的事件从 kqueue 中移除
  • 过滤器标识值:
    • EV_ENABLE:过滤器事件可用,注册一个事件时,默认是可用的。
    • EV_DISABLE:过滤器事件不可用,当内部描述可读或可写时,将不通知应用程序。

三、iocp

  1. 定义
          输入输出完成端口(Input/Output Completion Port,IOCP), 是支持多个同时发生的异步I/O操作的应用程序编程接口,在Windows NT的3.5版本以后,或AIX 5版以后或Solaris第十版以后,开始支持。
  2. 原理
          通常情况下,线程池中的工作线程的数量与CPU内核数量相同,以此来最小化线程切换代价。一个IOCP对象,在操作系统中可关联着多个Socket和(或)文件控制端。IOCP对象内部有一个先进先出(FIFO)队列,用于存放IOCP所关联的输入输出端的服务请求完成消息。请求输入输出服务的进程不接收IO服务完成通知,而是检查IOCP的消息队列以确定IO请求的状态。 (线程池中的)多个线程负责从IOCP消息队列中取走完成通知并执行数据处理;如果队列中没有消息,那么线程阻塞挂起在该队列。这些线程从而实现了负载均衡。

四、参考

  1. epoll详解
  2. epoll和kqueue