Linux Epoll 与 Java NIO的底层实现

Epoll

在Linux系统中,实现IO多路复用的机制有select、poll、epoll,其中epoll性能表现最为优秀。这里的复用指的是对进程的复用。

使用示例和epoll核心API详解

我们先从一个使用示例出发,接着探究epoll如何在底层做了支持。epoll使用示例:

       #define MAX_EVENTS 10
       struct epoll_event ev, events[MAX_EVENTS];
       int listen_sock, conn_sock, nfds, epollfd;

       /* Code to set up listening socket, 'listen_sock',
          (socket(), bind(), listen()) omitted. */

       epollfd = epoll_create1(0);
       if (epollfd == -1) {
           perror("epoll_create1");
           exit(EXIT_FAILURE);
       }

       ev.events = EPOLLIN;
       ev.data.fd = listen_sock;
       if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
           perror("epoll_ctl: listen_sock");
           exit(EXIT_FAILURE);
       }

       for (;;) {
           nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
           if (nfds == -1) {
               perror("epoll_wait");
               exit(EXIT_FAILURE);
           }

           for (n = 0; n < nfds; ++n) {
               if (events[n].data.fd == listen_sock) {
                   conn_sock = accept(listen_sock,
                                      (struct sockaddr *) &addr, &addrlen);
                   if (conn_sock == -1) {
                       perror("accept");
                       exit(EXIT_FAILURE);
                   }
                   setnonblocking(conn_sock);
                   ev.events = EPOLLIN | EPOLLET;
                   ev.data.fd = conn_sock;
                   if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                               &ev) == -1) {
                       perror("epoll_ctl: conn_sock");
                       exit(EXIT_FAILURE);
                   }
               } else {
                   do_use_fd(events[n].data.fd);
               }
           }
       }

上面是官方手册里给出的一个使用示例,简单做一下解读:

  • 获取要监听的socket

要使用epoll,我们首先要有一个用来监听(接收新连接)的socket,listen_sock, 这个socket一般通过socket()、bind()、listen()等一系列系统调用得到。

  • int epoll_create(int size) 和 int epoll_create1(int flags)

epoll_create函数的size参数在Linux 2.6.8之后已经没有用了,但是必须大于0。epoll_create1函数对epoll_create进行了扩展。当flags=0,即当调用epoll_create1(0)时,
等同于调用epoll_create。flags传入其他值(如flags = EPOLL_CLOEXEC)时,会有不同的功能。

epoll_create函数创建了一个epoll对象,返回一个指向epoll对象的文件描述符。这个文件描述符(示例代码中的epollfd)很重要,是使用者与epoll机制交互的重要数据。

  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

增加、修改、删除epoll对象的interest list,为目标文件描述符fd执行op操作,返回0表示成功,-1表示失败。op可以为以下值之一:

EPOLL_CTL_ADD

在epoll的interest list中增加一个条目,这个条目包含目标文件描述符fd和在event参数中的一些设置。

EPOLL_CTL_MOD

修改interest list中fd的设置置。

EPOLL_CTL_DEL

将fd从interest list中移除,此时event参数没有用。

结构体epoll_event的定义如下:

    typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
    } epoll_data_t;

    struct epoll_event {
        uint32_t     events;      /* Epoll events */
        epoll_data_t data;        /* User data variable */
    };

epoll_event的events成员,可以为以下值之一:

EPOLLIN

EPOLLOUT

EPOLLRDHUP

EPOLLPRI

EPOLLERR

EPOLLHUP

EPOLLET

EPOLLONESHOT

EPOLLWAKEUP

EPOLLEXCLUSIVE

这些事件就不详细介绍了,具体可以参考epoll_ctl的官方手册

  • epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

等待epfd指向的epoll对象上的events事件发生。epoll_wait会阻塞调用进程直到以下事件发生:

文件描述符投递了一个事件过来

调用被一个signal handler中断

超时

timeout=-1时,epoll_wait会一直等待;timeout=0时,epoll_wait立即返回。

内核如何实现epoll

从accept说起

当accept之后,服务端进程会创建一个socket出来,用于与客户端通信,然后把socket放到当前进程的打开文件列表中。socket定义在include/linux/net.h中,

struct socket {
    socket_state        state;
    short            type;
    unsigned long        flags;
    struct file        *file;
    struct sock        *sk;
    const struct proto_ops    *ops;
    struct socket_wq    wq;
};

socket的源码非常复杂,这里我们只关注跟epoll机制紧密相关的一些数据结构。如下图:

可以看到,accept后,会生成一个socket,这个socket关联了一个等待队列和数据ready时的回调函数(sock_def_readable)。

epoll_create做了什么

当用户进程调用了epoll_create后,内核会创建一个struct eventpoll的内核对象,同样放到当前进程的打开文件列表中。eventpoll定义在fs/eventpoll.c中,

struct eventpoll {
    struct mutex mtx;
    /* Wait queue used by sys_epoll_wait() */
    wait_queue_head_t wq;
    /* Wait queue used by file->poll() */
    wait_queue_head_t poll_wait;
    /* List of ready file descriptors */
    struct list_head rdllist;
    /* Lock which protects rdllist and ovflist */
    rwlock_t lock;
    /* RB tree root used to store monitored fd structs */
    struct rb_root_cached rbr;
    /**省略 */
}

epoll_create主要是进行eventpoll对象的初始化工作,这其中就包含了wq、rdllist、rbr成员。

epoll_ctl做了什么

epoll_ctl是epoll机制的关键实现。直观的来理解,epoll_ctl执行完成后,内核中epoll相关的对象得以逐步串联起来。epoll_ctl完成的功能主要有:

  • 分配一个红黑树节点对象(struct epitem)
  • 在socket的等待队列中加入一个元素,该元素中有个关键数据-回调函数ep_poll_callback。在socket数据准备好时,调用该函数执行epoll相关的操作。
  • 将epitem加入红黑树

下面这张图是笔者阅读源码后总结的,描述了epoll_ctl执行的一些关键动作。可能画的有些随意,如果读者能从中获取到以下两个信息:

  • epitem及关联的数据结构
  • socket的等待队列如何构成的

那么,就能比较容易的理解epoll_ctl的功能。

如果读者觉得上面这张图过于复杂和不好理解,下面这张概念性的图也描述了epoll_ctl的功能。

Java NIO实现

参考资料