带着 6 个问题深入理解 epoll

epoll 是一种多路复用技术。多路复用技术即一个线程可以监听多个文件句柄的的事件,多路指的是多个句柄,复用是指多个句柄复用一个线程资源。可想而知,和多路复用技术相对的就是一个文件句柄使用一个线程来监听。多路复用技术适用于高并发的场景。除了 epoll, linux 中的多路复用技术还有 poll 和 select。

nginx, libevent, golang net 库中均使用了 epoll。

本文首先通过一个简单例子来了解 epoll 基本使用方法,然后介绍了 epoll 中使用的主要数据结构,最后重点记录了 6 个问题,通过这 6 个问题全面而深入地理解 epoll。

1 epoll 使用方式

1.1 三个系统调用

通过以下 3 个 api 使用 epoll。

epoll 通过这 3 个 api 把控制面和数据面进行了隔离, epoll_cerate() 和 epoll_ctl() 完成控制面的工作,epoll_wait() 完成数据面的工作。控制面的工作包括 epoll 的创建,向 epoll 中增加 fd 或者从 epoll 中删除 fd,修改监听事件;数据面的工作包括从内核中获取事件,完成数据的读写。

epoll_create()

① 创建一个 epoll 对象,在内核会创建一个 eventpoll。

② 返回的是一个 fd(文件描述符),该 fd 代表内核态的一个 eventpoll 对象,后边 epoll_ctl(), epoll_wait() 都是基于这个 fd 进行操作; 用 fd 表示内核态的 epoll 对象,体现了 linux 一切皆文件的思想。

epoll_ctl()

这个 api 通过三个命令字完成控制操作:

① EPOLL_CTL_ADD

往 epoll 对象增加一个 fd,fd 加到 epoll 里边之后,epoll 才会对这个 fd 上的事件进行监听。

② EPOLL_CTL_DEL

语义与 EPOLL_CTL_ADD 相反

③ EPOLL_CTL_MOD

修改已经加入到 epoll 里边的 fd 需要监听的事件类型。

epoll_wait()

从内核获取事件。

1.2 测试代码

测试步骤:

① 如果服务端和客户端不在一台机器,需要将 epoll_client.c 中的 SERVER_IP 修改为服务端机器的 ip 地址;

② 编译

gcc epoll_server.c -o server

gcc epoll_client.c -o client

③ 先后启动 server 和 client,便可以看到两端有数据收发。

server 端代码(epoll_server.c):

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>

#define SERVER_IP ("0.0.0.0")  // 0.0.0.0 all local ip
#define SERVER_PORT (12345)
#define MAX_LISTENQ (32)
#define MAX_EVENT (128)
#define MAX_BUFSIZE (512)

int create_tcp_server() {
  int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
  if (listen_fd < 0) {
    printf("create socket error: %s\n", strerror(errno));
    return -1;
  }

  struct sockaddr_in server_addr;
  memset(&server_addr, 0, sizeof(server_addr));
  server_addr.sin_family = AF_INET;
  server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
  server_addr.sin_port = htons(SERVER_PORT);

  if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
    printf("bind[%s:%d] error.\n", SERVER_IP, SERVER_PORT);
    return -1;
  }

  if (listen(listen_fd, MAX_LISTENQ) < 0) {
    printf("listen error.\n");
    return -1;
  }

  return listen_fd;
}

int main() {
  int ret = -1;
  int sock_fd = -1;
  int accetp_fd = -1;
  int event_num = -1;
  int epoll_fd = -1;
  int listen_fd = -1;

  struct sockaddr_in client_addr;
  struct sockaddr_in server_addr;
  socklen_t client = sizeof(struct sockaddr_in);

  struct epoll_event ev;
  struct epoll_event events[MAX_EVENT];

  listen_fd = create_tcp_server();
  if (listen_fd < 0) {
    printf("create server error\n");
    return -1;
  }

  epoll_fd = epoll_create(MAX_EVENT);
  if (epoll_fd <= 0) {
    printf("cteare epoll failed, error: %s\n", strerror(errno));
    return -1;
  }

  ev.data.fd = listen_fd;
  ev.events = EPOLLIN;
  if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) < 0) {
    printf("add listen fd to epoll error.\n");
    return -1;
  }

  char buff[MAX_BUFSIZE] = {'\0'};
  for (;;) {
    printf("epoll wait:\n");
    event_num = epoll_wait(epoll_fd, events, MAX_EVENT, -1);
    printf("events num:%d\n", event_num);
    for (int i = 0; i < event_num; i++) {
      if (events[i].events & EPOLLIN == 0) {
        printf("fd %d error, cloase it, event[0x%x].\n", events[i].data.fd, events[i].events);
        close(events[i].data.fd);
        return -1;
      }

      if (events[i].data.fd == listen_fd) {
        accetp_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client);
        printf("accepted fd: %d\n", accetp_fd);
        if (accetp_fd < 0) {
          printf("accept error.\n");
          return -1;
        }

        ev.data.fd = accetp_fd;
        ev.events = EPOLLIN;
        if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, accetp_fd, &ev) < 0) {
          printf("add fd to epoll error.\n");
          return -1;
        }
      } else {
        ret = recv(accetp_fd, buff, MAX_BUFSIZE, 0);
        if (ret <= 0) {
          printf("recv error. %s\n", strerror(errno));
          epoll_ctl(epoll_fd, EPOLL_CTL_DEL, accetp_fd, &ev);
          close(accetp_fd);
          events[i].data.fd = -1;
          return -1;
        }

        buff[ret] = '\0';
        printf("**recv data: %s\n", buff);
        send(accetp_fd, buff, strlen(buff), 0);
      }
    }
  }

  return 0;
}

 client 端代码(epoll_client.c):

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#define SERVER_PORT (12345)
#define SERVER_IP "127.0.0.1"
#define MAX_EVENT (128)
#define MAX_BUFSIZE (512)

int main(int argc, char *argv[]) {
  int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
  if (sock_fd < 0) {
    printf("create socket error.\n");
    return -1;
  }

  struct sockaddr_in addr_serv;
  memset(&addr_serv, 0, sizeof(addr_serv));

  addr_serv.sin_family = AF_INET;
  addr_serv.sin_port = htons(SERVER_PORT);
  addr_serv.sin_addr.s_addr = inet_addr(SERVER_IP);

  if (connect(sock_fd, (struct sockaddr *)&addr_serv, sizeof(struct sockaddr)) < 0) {
    printf("connect error.\n");
    return -1;
  }

  int epoll_fd;
  struct epoll_event ev;
  struct epoll_event events[MAX_EVENT];
  epoll_fd = epoll_create(MAX_EVENT);
  if (epoll_fd <= 0) {
    printf("create epoll error.\n");
    return 0;
  }

  ev.data.fd = sock_fd;
  ev.events = EPOLLIN;
  if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &ev) < 0) {
    printf("add fd to epoll error.\n");
    return -1;
  }

  int recv_num;
  char send_buff[] = "epoll test!";
  char recv_buff[MAX_BUFSIZE];
  send(sock_fd, send_buff, strlen(send_buff), 0);
  for (;;) {
    printf("epoll wait:\n");
    int event_num = epoll_wait(epoll_fd, events, MAX_EVENT, -1);
    printf("epoll events num:%d\n", event_num);

    for (int i = 0; i < event_num; i++) {
      if (events[i].events & EPOLLIN == 0) {
        printf("fd %d error, cloase it, event[0x%x].\n", events[i].data.fd, events[i].events);
        close(events[i].data.fd);
        return -1;
      }

      recv_num = recv(sock_fd, recv_buff, sizeof(recv_buff), 0);
      if (recv_num < 0) {
        printf("recv error\n");
        return -1;
      }

      recv_buff[recv_num] = '\0';
      printf("**recv data: %s\n", recv_buff);
      send(sock_fd, send_buff, strlen(send_buff), 0);
    }
  }

  close(sock_fd);
  return 0;
}

 2 epoll 内核数据结构

介绍两个数据结构,struct eventpoll 和 struct epitem。

前者在内核中代表一个 epoll;后者在内核中表示一个 epoll 监听的对象,比如一个 tcp 的 socket。在 c 语言中常常用结构体来表示一个对象,结构体中可以定义对象的属性,还可以定义函数指针表示对象的方法。

2.1 eventpoll

eventpoll 是一个结构体, 代表整个 epoll 对象,epoll_create() 的时候会创建一个 struct eventpoll,并对结构体的成员进行初始化。

为了便于理解,下边只展示了 5 个成员,重点需要理解的是 rbr 和 rdllist。

struct eventpoll {
    /* 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;

    /* RB tree root used to store monitored fd structs */
    struct rb_root_cached rbr;

    /*
     * This is a single linked list that chains all the "struct epitem" that
     * happened while transferring ready events to userspace w/out
     * holding ->lock.
     */
    struct epitem *ovflist;
};

下面分别对这 5 个成员进行介绍:

(1)rbr

rbr 是红黑树,保存着所有需要 epoll 监听的 fd 信息。红黑树中的每个节点是一个 epitem,epitem 代表着一个fd。

红黑树同时也是二叉搜索树,需要对节点进行排序,每个节点都有一个 key 用于在红黑树中进行排序。eventpoll 的红黑树节点的 key 是文件描述符在内核中对应的 file 结构体的地址。

static inline int ep_cmp_ffd(struct epoll_filefd *p1,
                 struct epoll_filefd *p2)
{
    return (p1->file > p2->file ? +1:
       (p1->file < p2->file ? -1 : p1->fd - p2->fd));
}

epoll 中通过 ep_insert(), ep_remove, ep_modify(), ep_find() 完成红黑树的增删改查操作。

红黑树在增,删,改,查操作的时间复杂度之间取得了一定的平衡。linux 内核在多处使用了红黑树,tcp 接收侧维护了一个乱序队列,用来缓存乱序的报文,直到报文连续之后才会将连续的报文上报给应用,其中的乱序队列使用红黑树来实现;tcp 发送侧有一个重传队列,也是使用红黑树来维护;linux 调度策略 SCHED_NORMAL 和 SCHED_DEADLINE 的调度队列也是使用红黑树来实现。

(2)rdllist 就绪队列

就绪队列可以看做 epoll 中事件流转的中枢,当一个 fd 上有事件,epoll 便会将该 fd 对应的 epitem 加入到就绪队列中。

就绪队列是一个双向循环链表,使用内核的 struct list_head 实现。

生产者:底层协议栈,底层协议栈如果有事件(读,写,错误),会通过 sock_def_readable() 等函数将对应的 epitem 添加到 rdllist 里边。

消费者:用户态调用 epoll_wait() ,epoll_wait() 会从这个链表中消费事件。

sock_def_readable(), sock_def_write_space(), sock_def_error_report() 这 3 个函数是协议栈向 sock 层通知事件的接口,分别是有可读,可写,错误。在创建 socket 的时候,都会调用函数 sock_init_data_uid(),在这个函数中完成了这几个函数的挂接。

void sock_init_data_uid(struct socket *sock, struct sock *sk, kuid_t uid)
{
        ...
	sk->sk_state_change	=	sock_def_wakeup;
	sk->sk_data_ready	=	sock_def_readable;
	sk->sk_write_space	=	sock_def_write_space;
	sk->sk_error_report	=	sock_def_error_report;
	sk->sk_destruct		=	sock_def_destruct;
        ...
}

(3) ovflist

从 ovflist 的注释可以看出来,如果当前正在向用户空间传递事件,那么这个时候底层协议栈上报的事件不会加入到 rdllist, 而是会加入到 ovflist 当中。

// ovflist 有两个取值, NULL 和 EP_UNACTIVE_PTR
// (1) EP_UNACTIVE_PTR:不需要将 epitem 放到 ovflist,
// UNACTIVE 即不活跃的
// 在函数 ep_done_scan() 中将 epitem 设置成 EP_UNACTIVE_PTR
// 调用 ep_done_scan() 的时候说明这次数据传输(从内核态向用户态)已经结束,可以将有事件的 epitem 放入 rdllist 中了
// (2) NULL: 需要将 epitem 放到 ovflist

// 在函数 ep_start_scan() 中将 ovflist 设置成 NULL,ep_start_scan() 和 ep_done_scan() 意思相反,
// 调用该函数时,即要开始进行数据传输了,这个时候需要将 epitem 放入 ovflist 当中
static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
{

    /*
     * If we are transferring events to userspace, we can hold no locks
     * (because we're accessing user memory, and because of linux f_op->poll()
     * semantics). All the events that happen during that period of time are
     * chained in ep->ovflist and requeued later on.
     */
    if (READ_ONCE(ep->ovflist) != EP_UNACTIVE_PTR) {
        if (chain_epi_lockless(epi))
            ep_pm_stay_awake_rcu(epi);
    } else if (!ep_is_linked(epi)) {
        /* In the usual case, add event to ready list. */
        if (list_add_tail_lockless(&epi->rdllink, &ep->rdllist))
            ep_pm_stay_awake_rcu(epi);
    }
}

每次 epoll_wait() 的时候,通过函数 ep_send_events() 来做事件的转移工作, 包括以下 3 步:

① 将事件从 rdllist 转移到局部变量 txlist 中

该函数将 rdllist 中的事件,通过指针操作,直接把事件转移到了 txlist 中,该函数返回之后,就会通过操作 txlist 将事件写入到用户空间的 event 数组当中, event 数组是用户调用 epoll_wait() 时传下来的数组;

同时将 ovflist 置 NULL,这样的话在 ep_poll_callback() 函数中就会把事件放到 ovflist 当中。

ep_start_scan(struct eventpoll *ep, struct list_head *txlist)
{
    ...
    lockdep_assert_irqs_enabled();
    write_lock_irq(&ep->lock);
    list_splice_init(&ep->rdllist, txlist);
    WRITE_ONCE(ep->ovflist, NULL);
    write_unlock_irq(&ep->lock);
    ...
}

 ② 将事件从 txlist 转移至用户的 event 数组

static int ep_send_events(struct eventpoll *ep,
              struct epoll_event __user *events, int maxevents)
{
    list_for_each_entry_safe(epi, tmp, &txlist, rdllink) {
        events = epoll_put_uevent(revents, epi->event.data, events);
    }
}

③ 将 ovflist 中的事件转移到 rdllist, 这样下次查询事件的时候,就能从 rdllist 中将这些事件取出

static void ep_done_scan(struct eventpoll *ep,
             struct list_head *txlist)
{
    for (nepi = READ_ONCE(ep->ovflist); (epi = nepi) != NULL;
         nepi = epi->next, epi->next = EP_UNACTIVE_PTR) {
        if (!ep_is_linked(epi)) {
            list_add(&epi->rdllink, &ep->rdllist);
            ep_pm_stay_awake(epi);
        }
    }
}

(4) wq

注释说的很清楚,wq 表示一个等待队列,在 sys_poll_wait() 当中使用,

意思就是 epoll_wait() 的时候,如果当时没有事件,并且设置了等待时间没有超时或者设置成了阻塞模式,这个时候进程就会等待事件,在让出 cpu 之前,线程会将自己加入到等待队列里边,待有事件了会被唤醒。

    /* Wait queue used by sys_epoll_wait() */
    wait_queue_head_t wq;

在 ep_poll_callback() 函数中,会将等待的线程唤醒,线程被唤醒之后就会去 rdllist 查找有没有事件,如果有的话,则将事件上报给用户空间并返回,否则会继续等待。

static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
{
    if (waitqueue_active(&ep->wq)) {
        wake_up(&ep->wq);
    }
}

(5)poll_wait

poll_wait 和上边的 wq 的数据结构是一样的,都是等待队列。当把 epoll fd 当成一个普通 fd 被监听时,poll_wait 这个成员才会使用(把 epoll fd 当成一个普通的 fd 被监听,后边 3.2 节会讨论)。

两者的区别如下:

① 服务的对象不同

wq 服务的对象是整个 epoll, 里边的元素代表一个线程,这个线程现在在睡眠,有事件上来之后会被唤醒,监听这个 epoll 的线程,都会加到这个队列里边,所以 wq 服务的是 epoll 这个对象。

poll_wait 服务的对象是一个 fd,比如一个 tcp socket,这样当该 socket 上边有事件的时候,会判断 poll_wait 有没有元素,如果有的话就进行唤醒。

② 唤醒函数不同

wq 是 autoremove_wake_function(), 这个函数里边会把 wq 中睡眠的线程唤醒。

#define init_wait(wait)                                \
    do {                                    \
        (wait)->private = current;                    \
        (wait)->func = autoremove_wake_function;            \
        INIT_LIST_HEAD(&(wait)->entry);                    \
        (wait)->flags = 0;                        \
    } while (0)

poll_wait 的唤醒函数是 ep_poll_callback(), 这个函数里边不会做唤醒的工作,而是把事件加入到 rdllist 里边。

③ 移除时机不同

wq 中的睡眠线程,每一次被唤醒都会从 wq 中移除,下次睡眠的时候再次加入。

poll_wait 中的元素,每次有事件并不会从队列里边移除,而是会在 EPOLL_CTL_DEL 的时候进行移除。

wait_queue_head_t 队列中的元素是 struct wait_queue_entry 类型,结构提中有一个成员是 func,挂的是唤醒函数。wq 中的唤醒函数是 autoremove_wake_function, poll_wait 中的唤醒函数是 ep_poll_callback。

struct wait_queue_entry {
    unsigned int        flags;
    void            *private;
    wait_queue_func_t    func;
    struct list_head    entry;
};

2.2 epitem

了解了 eventpoll 中的成员之后,epitem 就好理解了,eventpoll 代表整个 epoll,是一个容器,epitem 代表一个被监听的文件描述符,是一个元素。

epitem 中的 rbn 对应 eventpoll 中的 rbr, rdllink 对应 rdllist,next 对应 ovflist。

struct epitem {
    union {
        /* RB tree node links this structure to the eventpoll RB tree */
        struct rb_node rbn;
        /* Used to free the struct epitem */
        struct rcu_head rcu;
    };

    /* List header used to link this structure to the eventpoll ready list */
    struct list_head rdllink;

    /*
     * Works together "struct eventpoll"->ovflist in keeping the
     * single linked chain of items.
     */
    struct epitem *next;

    /* The file descriptor information this item refers to */
    struct epoll_filefd ffd;

    /* List containing poll wait queues */
    struct eppoll_entry *pwqlist;

    /* The "container" of this item */
    struct eventpoll *ep;

    /* List header used to link this item to the "struct file" items list */
    struct hlist_node fllink;


    /* The structure that describe the interested events and the source fd */
    struct epoll_event event;
};

3 六个问题

3.1 epoll 回调函数是哪个,挂在了哪里,什么时候会调用

(1)回调函数是哪个 ?

ep_poll_callback()

另外, epoll 中还有一个函数 ep_ptable_queue_proc() 在这个函数中真正完成回调函数的挂载。

(2)挂在了哪里

对于 socket 来说,回调函数挂在了 socket 结构体的等待队列 wq 中。

struct socket {
    struct socket_wq    wq;
};

ep_poll_callback() 的挂接是在 epoll 添加 fd 的时候完成的,下边记录一下回调挂接的过程:

① epoll_ctl() 调用 ep_insert() 完成监听 fd 的添加,具体的添加工作在 ep_insert() 中完成。

② ep_insert() 中调用函数 init_poll_funcptr(), 初始化 poll_table

poll_table 中的主要成员是 _qproc,在这个成员上挂了函数 ep_ptable_queue_proc(),在函数 ep_ptable_queue_proc() 中真正完成回调函数(ep_poll_callback)的挂接工作。

ep_insert():

初始化 pt, 然后调用 ep_item_poll(), 同时入参传递了 pt:

static int ep_insert(struct eventpoll *ep, const struct epoll_event *event,
             struct file *tfile, int fd, int full_check)
{
    epq.epi = epi;
    init_poll_funcptr(&epq.pt, ep_ptable_queue_proc); // 初始化 pt

    revents = ep_item_poll(epi, &epq.pt, 1);
}

ep_item_poll():

调用 vfs_poll() 同时入参传递了 pt,对于 tcp socket 来说就是调用 tcp_poll():

static __poll_t ep_item_poll(const struct epitem *epi, poll_table *pt,
                 int depth)
{
    res = vfs_poll(file, pt);
}

tcp_poll():

调用 sock_poll_wait() 同时入参传递了 pt,在这里名字改成了 wait。

__poll_t tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
    sock_poll_wait(file, sock, wait);
}

sock_poll_wait():

在该函数中取出了 socket 的等待队列,即 sock->wq.wait,然后调用 poll_wait(),同时传递了 pt,在这里名字改成了 p。

static inline void sock_poll_wait(struct file *filp, struct socket *sock,
                  poll_table *p)
{
    if (!poll_does_not_wait(p)) {
        poll_wait(filp, &sock->wq.wait, p);
        smp_mb();
    }
}

poll_wait():

在这个函数里边,上边函数调用过程中一直传递的 pt 终于排上了用场,调用在 epoll 中注册的函数 ep_ptable_queue_proc() 来完成真正的 ep_poll_callback() 挂接的工作。

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
    if (p && p->_qproc && wait_address)
        p->_qproc(filp, wait_address, p);
}

ep_ptable_queue_proc() :

可以看到在该函数中,封装了一个 pwq,里边就把 ep_poll_callback() 封装了进来,然后把这个元素添加到了等待队列里边。

static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
                 poll_table *pt)
{
    struct ep_pqueue *epq = container_of(pt, struct ep_pqueue, pt);
    struct epitem *epi = epq->epi;
    struct eppoll_entry *pwq;

    if (unlikely(!epi))    // an earlier allocation has failed
        return;

    pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL);
    if (unlikely(!pwq)) {
        epq->epi = NULL;
        return;
    }

    init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
    pwq->whead = whead;
    pwq->base = epi;
    if (epi->event.events & EPOLLEXCLUSIVE)
        add_wait_queue_exclusive(whead, &pwq->wait);
    else
        add_wait_queue(whead, &pwq->wait);
    pwq->next = epi->pwqlist;
    epi->pwqlist = pwq;
}

(3)什么时候会调用

回调的调用是在函数 sock_def_readable()、sock_def_write_space()、sock_def_error_report() 中发生的,这几个函数在 socket 初始化的时候,会挂到 sock 的函数指针上,当底层协议栈中有事件发生时就会调用对用的函数,向上层通知事件。

以读事件为例:

sock_def_readable():

在这个函数中首先取出 sock 的等待队列,然后调用 wake_up_interruptible_sync_poll() 唤醒等待队列里边的等待元素。

void sock_def_readable(struct sock *sk)
{
    struct socket_wq *wq;

    rcu_read_lock();
    wq = rcu_dereference(sk->sk_wq);
    if (skwq_has_sleeper(wq))
        wake_up_interruptible_sync_poll(&wq->wait, EPOLLIN | EPOLLPRI |
                        EPOLLRDNORM | EPOLLRDBAND);
    sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
    rcu_read_unlock();
}

看到这里有个疑问,上边说的 epoll 的回调也是挂在了 socket 的等待队列中,挂到了 struct socket 这个结构体当中的 wq 中;这里要唤醒的等待队列却是 struct sock 中的 sk_wq,这两个不一样啊,怎么解释。

其实这两者指向的是一个队列,在 sock 初始化函数中做了关联。

void sock_init_data(struct socket *sock, struct sock *sk)
{
    if (sock) {
        sk->sk_type    =    sock->type;
        RCU_INIT_POINTER(sk->sk_wq, &sock->wq); // 将 socket->wq 赋值给 sock->sk_wq
        sock->sk    =    sk;
        sk->sk_uid    =    SOCK_INODE(sock)->i_uid;
    }
}

__wake_up_common():

通过如下函数调用关系调用到 __wake_up_common()

__wake_up_sync_key() --> __wake_up_common_lock() --> __wake_up_common()

在该函数中就会取出等待队列中的元素并调用对应的 func 回调函数。对于 epoll 来说,回调函数就是 ep_poll_callback()。

static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
            int nr_exclusive, int wake_flags, void *key,
            wait_queue_entry_t *bookmark)
{

    list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
        ret = curr->func(curr, mode, wake_flags, key);
    }
}

epoll 和 socket 进行通信的方式就是通过 socket 中 wq 来进行。epoll 向 socket 的 wq 中添加了元素,当 socket 有事件的时候就会遍历 wq 中的元素,调用对应的函数,相当于向所有的监听方通知这个事件。类似于设计模式中的观察者模式。

linux 代表资源的这些对象,比如 socket,epoll,mutex,用户态使用的条件变量。当这些资源没有到来的时候,资源的消费者就需要等待,对应的就有一个等待队列;资源的生产者生产资源的时候,便会唤醒等待队列中的元素,唤醒的动作,往往是生产者注册的一个回调函数,具体的唤醒逻辑在回调函数中实现。

 3.2 一个 epoll fd 可以当作一个普通的 fd 被另一个 epoll 监听吗

可以

这种使用方式,自己没见过具体的使用场景,但是 epoll 是支持这种用法的。

epoll 最常用的方式是监听普通的 socket fd,比如建立了 100 条 tcp 连接,然后把每个连接加入到 epoll 中,通过 epoll 进行监听,如下图所示。

epoll 也是用一个 fd 来表示,epoll fd 也可以当做一个普通的 fd 加入到另外一个 epoll 里边进行监听。如下图所示,有 200 个 tcp 连接,其中第 1 到 100 个被 epoll1 监听,第 101 到 200 个被 epoll2 监听,然后 epoll1 和 epoll 2 再被 epoll 3 监听,这样用户只监听 epoll3 来同时监听 200 个连接。

当一个对象实现了 struct file_operations 中的 poll() 方法时,这个对象就可以加入到 epoll 中监听,否则无法使用 epoll 监听。如下是 epoll 实现的 struct_operations,其中实现了 poll 方法,所以 epoll 也可以当做一个普通 fd 被另一个 epoll 监听。

/* File callbacks that implement the eventpoll file behaviour */
static const struct file_operations eventpoll_fops = {
#ifdef CONFIG_PROC_FS
	.show_fdinfo	= ep_show_fdinfo,
#endif
	.release	= ep_eventpoll_release,
	.poll		= ep_eventpoll_poll,
	.llseek		= noop_llseek,
};

 这种使用方式下,代码也需要分两级来进行:

event_num = epoll_wait(epoll3, events, MAX_EVENT, -1);
for (int i = 0; i < event_num; i++ ) {
    if (events[i].data.fd == epoll1) {
        epoll_wait(epoll1, events, MACX_EVENT, -1);
        ....
    } else if (events[i].data.fd == epoll2) {
        epoll_wait(epoll1, events, MACX_EVENT, -1);
        ....
    }
}

通过代码来理解:

(1) epoll1, epoll2 加入到 epoll3 和普通的 fd1 加入到 epoll1,代码上有什么区别

无论是普通的 fd 还是 epoll fd, 需要被 epoll 监听的时候,均是向这个 fd 的 poll_wait 队列入队一个元素,即注册回调函数 ep_poll_callback(),当描述符中有事件的时候,则会调用这个回调函数进行事件通知。

回调注册在 ep_item_poll() 函数中完成,普通 fd 通过 vfs_poll() 完成,epoll fd 通过 __ep_eventpoll_poll() 完成。

static __poll_t ep_item_poll(const struct epitem *epi, poll_table *pt,
                 int depth)
{
    struct file *file = epi->ffd.file;
    __poll_t res;

    pt->_key = epi->event.events;
    if (!is_file_epoll(file))
        res = vfs_poll(file, pt);
    else
        res = __ep_eventpoll_poll(file, pt, depth);
    return res & epi->event.events;
}

(2) 当 fd1 上有事件的时候,是怎么通知到 epoll3 的

epoll3 被通知到有事件,需要经过两步骤:

第一步:fd1 上有事件,通知 epoll1

调用栈如下所示,底层协议栈收到数据之后通过 ep_poll_callback() 通知 epoll

0xffffffff8b9281e0 : ep_poll_callback+0x0/0x2c0 [kernel]

0xffffffff8b7027fa : __wake_up_common+0x7a/0x190 [kernel]

0xffffffff8b70298c : __wake_up_common_lock+0x7c/0xc0 [kernel]

0xffffffff8bd1cf07 : sock_def_readable+0x37/0x60 [kernel]

0xffffffff8bdd7243 : tcp_data_queue+0x233/0xb60 [kernel]

0xffffffff8bdd7d90 : tcp_rcv_established+0x220/0x620 [kernel]

0xffffffff8bde3c8a : tcp_v4_do_rcv+0x12a/0x1e0 [kernel]

0xffffffff8bde5b2d : tcp_v4_rcv+0xb3d/0xc40 [kernel]

0xffffffff8bdbafc9 : ip_local_deliver_finish+0x69/0x210 [kernel]

0xffffffff8bdbb2eb : ip_local_deliver+0x6b/0xe0 [kernel]

0xffffffff8bdbb5db : ip_rcv+0x27b/0x36a [kernel]

0xffffffff8bd3ebe1 : __netif_receive_skb_core+0xb41/0xc40 [kernel]

0xffffffff8bd3ed7d : netif_receive_skb_internal+0x3d/0xb0 [kernel]

0xffffffff8bd3f67a : napi_gro_receive+0xba/0xe0 [kernel]

第二步:epoll1 上有事件,通知 epoll3

这个通知在 ep_poll_callback 中发起,由上文可知,epoll1 加入到 epoll3 的时候,epoll3 在 epoll1 的 poll_wait 中入队了一个元素,所以这个时候,发现 epoll1 的 poll_wait 有元素,则通过 ep_poll_safe_wake() 调用 epoll3 的回调,同样最终会调到 ep_poll_callback() 将事件挂到 epoll 的就绪队列中,这样 epoll3 就能监听到事件了。

static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
{
   ... 
   if (waitqueue_active(&ep->poll_wait))
    pwake++;
    ...
    if (pwake)
    ep_poll_safewake(ep, epi);

    ...
}

(3) 深度限制

由上边的分析,可以知道,当一个 epoll 被另外一个 epoll 监听的时候,事件上报路径会增加一级。epoll 中在向 epoll 添加需要被监听的描述符时,会对事件上报路径深度进行检查,如果检查不通过,则会添加失败。

引起上报深度过长的情况由两种:

① 循环路径,epoll1 监听 epoll2, 同时 epoll2 监听 epoll1

② 路径过长,如下边右边这张图,epoll1 被 epoll2 监听,epoll2 被 epoll3 监听 ...,epoll5 被 epoll6 监听。

代码验证:

如下代码能验证以上两种情况,将 注释 1 和 注释 2 同时打开,能验证循环监听的情况; 将 注释 1 打开,注释 2 不打开,能验证监听路径太长的情况。

#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>

#define SERVER_IP     ("0.0.0.0")
#define SERVER_PORT   (12345)
#define MAX_LISTENQ   (32)
#define MAX_EVENT     (128)

int create_tcp_server() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        printf("create socket error: %s\n", strerror(errno));
        return -1;
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); /**< 0.0.0.0 all local ip */
    server_addr.sin_port = htons(SERVER_PORT);

    if (bind(listen_fd,(struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        printf("bind[%s:%d] error.\n", SERVER_IP, SERVER_PORT);
        return -1;
    }

    if (listen(listen_fd, MAX_LISTENQ) < 0) {
        printf("listen error.\n");
        return -1;
    }

    return listen_fd;
}

int main() {
    int ret = -1;
    int sock_fd = -1;
    int accetp_fd = -1;
    int event_num = -1;
    int fd1 = -1;

    struct sockaddr_in client_addr;
    struct sockaddr_in server_addr;
    socklen_t client = sizeof(struct sockaddr_in);

    struct epoll_event ev;
    struct epoll_event events[MAX_EVENT];

    fd1 = create_tcp_server();
    if (fd1 < 0) {
        printf("create server error\n");
        return -1;
    }

    int epoll1 = epoll_create(MAX_EVENT);
    if (epoll1 <= 0) {
        printf("cteare epoll1 failed, error: %s\n", strerror(errno));
        return -1;
    }

    int epoll2 = epoll_create(MAX_EVENT);
    if (epoll2 <= 0) {
        printf("cteare epoll2 failed, error: %s\n", strerror(errno));
        return -1;
    }

    int epoll3 = epoll_create(MAX_EVENT);
    if (epoll3 <= 0) {
        printf("cteare epoll3 failed, error: %s\n", strerror(errno));
        return -1;
    }

    int epoll4 = epoll_create(MAX_EVENT);
    if (epoll4 <= 0) {
        printf("cteare epoll4 failed, error: %s\n", strerror(errno));
        return -1;
    }

    int epoll5 = epoll_create(MAX_EVENT);
    if (epoll5 <= 0) {
        printf("cteare epoll5 failed, error: %s\n", strerror(errno));
        return -1;
    }

    int epoll6 = epoll_create(MAX_EVENT);
    if (epoll6 <= 0) {
        printf("cteare epoll6 failed, error: %s\n", strerror(errno));
        return -1;
    }

    ev.data.fd = fd1;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll1, EPOLL_CTL_ADD, fd1, &ev) < 0) {
        printf("add fd1 to epoll1 error: %s\n", strerror(errno));
        return -1;
    }
    
    // 注释 1
    /* 
    ev.data.fd = epoll1;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll2, EPOLL_CTL_ADD, epoll1, &ev) < 0) {
        printf("add epoll1 to epoll2 error: %s\n", strerror(errno));
        return -1;
    }
    */
    
    // 注释 2
    /*
    ev.data.fd = epoll2;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll1, EPOLL_CTL_ADD, epoll2, &ev) < 0) {
        printf("add epoll2 to epoll1 error: %s\n", strerror(errno));
        return -1;
    }
    */

    ev.data.fd = epoll2;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll3, EPOLL_CTL_ADD, epoll2, &ev) < 0) {
        printf("add epoll2 to epoll3 error: %s\n", strerror(errno));
        return -1;
    }

    ev.data.fd = epoll3;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll4, EPOLL_CTL_ADD, epoll3, &ev) < 0) {
        printf("add epoll3 to epoll4 error: %s\n", strerror(errno));
        return -1;
    }
    
    ev.data.fd = epoll4;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll5, EPOLL_CTL_ADD, epoll4, &ev) < 0) {
        printf("add epoll4 to epoll5 error: %s\n", strerror(errno));
        return -1;
    }

    ev.data.fd = epoll5;
    ev.events = EPOLLIN;
    if (ret = epoll_ctl(epoll6, EPOLL_CTL_ADD, epoll5, &ev) < 0) {
        printf("add epoll5 to epoll6 error: %s, ret: %d, epoll5: %d, epoll6: %d\n", strerror(errno), ret, epoll5, epoll6);
        return -1;
    }

    return 0;
}

 循环路径报错信息以及代码位置:

int do_epoll_ctl(int epfd, int op, int fd, struct epoll_event *epds,
         bool nonblock)
{

    error = epoll_mutex_lock(&ep->mtx, 0, nonblock);
    if (error)
        goto error_tgt_fput;
    if (op == EPOLL_CTL_ADD) {
        if (READ_ONCE(f.file->f_ep) || ep->gen == loop_check_gen ||
            is_file_epoll(tf.file)) {
            mutex_unlock(&ep->mtx);
            error = epoll_mutex_lock(&epmutex, 0, nonblock);
            if (error)
                goto error_tgt_fput;
            loop_check_gen++;
            full_check = 1;
            if (is_file_epoll(tf.file)) {
                tep = tf.file->private_data;
                error = -ELOOP; // 循环路径报错
                if (ep_loop_check(ep, tep) != 0)
                    goto error_tgt_fput;
            }
            error = epoll_mutex_lock(&ep->mtx, 0, nonblock);
            if (error)
                goto error_tgt_fput;
        }
    }
    return error;
}

 路径太长报错信息及报错位置:

static int ep_insert(struct eventpoll *ep, const struct epoll_event *event,
             struct file *tfile, int fd, int full_check)
{
    /* now check if we've created too many backpaths */
    if (unlikely(full_check && reverse_path_check())) {
        ep_remove(ep, epi);
        return -EINVAL;
    }
    return 0;
}

 3.3 水平触发和边沿触发的区别

通俗来讲,水平触发,只要有数据,epoll_wait() 就会一直返回;边沿触发,如果数据没处理完,并且这个 fd 没有新的事件,那么再次 epoll_wait() 的时候也不会有事件上来。

水平触发还是边沿触发,影响的是接收事件的行为。

(1)举个例子

假如一个 tcp socket 一次收到了 1MByte 的数据,但是 epoll_wait() 返回之后,用户一次只读取 1KB 的数据,读完之后再进行 epoll_wait() 监听事件,示例代码如下所示:

    for (;;) {
        event_num = epoll_wait(epoll_fd, events, 1, -1);
        ret = recv(tcp_fd, buff, 1024, 0);
        if (ret <= 0) {
            break;
       }
    }

如果是水平触发,那么每次 epoll_wait() 都能返回这个 tcp_fd 的事件,共返回 (1MB / 1KB)= 1024 次事件;如果是边沿触发,那么只有第一次返回了 tcp_fd 的事件,后边的 epoll_wait() 也不返回了。

(2)代码

从代码实现来理解边沿触发和水平触发之间的区别。

用户在调用 epoll_wait() 之后,如果有事件,最终会调用 ep_send_events() 来做事件的转移,

主要工作是将事件从 rdllist 中转移到用户的 events 数组。

static int ep_send_events(struct eventpoll *ep,
              struct epoll_event __user *events, int maxevents)
{
    ep_start_scan(ep, &txlist);
    list_for_each_entry_safe(epi, tmp, &txlist, rdllink)
        revents = ep_item_poll(epi, &pt, 1);
        if (!revents)
            continue;

        events = epoll_put_uevent(revents, epi->event.data, events);
    }
    res++;
    if (epi->event.events & EPOLLONESHOT)
        epi->event.events &= EP_PRIVATE_BITS;
    else if (!(epi->event.events & EPOLLET)) { // 如果不是边沿触发,则将 epitem 重新加回就绪链表
        list_add_tail(&epi->rdllink, &ep->rdllist);
        ep_pm_stay_awake(epi);
    }
    return res;
}

对于从 rdllist 中获取的 epitem,首先调用 ep_item_poll() 确定有没有事件,以及事件的具体类型,如果有事件则件通过 epoll_put_uevent() 将事件保存存到用户的 event 数组中。

之后会判断是否为边沿触发,如果不是边沿触发,即水平触发,还会把这个 epitem 加入到 rdllist 的尾部,这样下次 epoll_wait() 的时候还会 poll 这个 fd,还会对这个 fd 进行 poll 操作,如果有事件还会向用户返回。

以上边举的例子来看,第 1 到 1024 次调用 epoll_wait() 都会返回事件,第 1025 次调用 epoll_wait() 的时候,由于数据都已经读取完毕,所以通过 ep_item_poll() 发现没有事件,这次当然也不会向用户返回事件,同时也不会把 epitem 返回 rdllist 了。

(3)实验 

server: 

#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>

#define SERVER_IP     ("0.0.0.0")
#define SERVER_PORT   (12345)
#define MAX_LISTENQ   (32)
#define MAX_EVENT     (128)

int create_tcp_server() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        printf("create socket error: %s\n", strerror(errno));
        return -1;
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); /**< 0.0.0.0 all local ip */
    server_addr.sin_port = htons(SERVER_PORT);

    if (bind(listen_fd,(struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        printf("bind[%s:%d] error.\n", SERVER_IP, SERVER_PORT);
        return -1;
    }

    if (listen(listen_fd, MAX_LISTENQ) < 0) {
        printf("listen error.\n");
        return -1;
    }

    return listen_fd;
}

int main() {
    int ret = -1;
    int sock_fd = -1;
    int accetp_fd = -1;
    int event_num = -1;
    int epoll_fd  = -1;
    int listen_fd = -1;

    struct sockaddr_in client_addr;
    struct sockaddr_in server_addr;
    socklen_t client = sizeof(struct sockaddr_in);

    struct epoll_event ev;
    struct epoll_event events[MAX_EVENT];

    listen_fd = create_tcp_server();
    if (listen_fd < 0) {
        printf("create server error\n");
        return -1;
    }

    epoll_fd = epoll_create(MAX_EVENT);
    if (epoll_fd <= 0) {
        printf("cteare epoll failed, error: %s\n", strerror(errno));
        return -1;
    }

    ev.data.fd = listen_fd;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) < 0) {
        printf("add listen fd to epoll error.\n");
        return -1;
    }

    char buff[1024] = {'\0'};
    int seq = 0;
    for (;;) {
        printf("epoll wait:\n");
        event_num = epoll_wait(epoll_fd, events, MAX_EVENT, -1);
        printf("events num:%d\n", event_num);
        for (int i = 0; i < event_num; i++ ) {
            if (events[i].events & EPOLLIN == 0) {
                /* 只监听 EPOLLIN 事件 */
                printf("fd %d error, cloase it, event[0x%x].\n", events[i].data.fd, events[i].events);
                close(events[i].data.fd);
                return -1;
            }
            
            if (events[i].data.fd == listen_fd) {
                accetp_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client);
                if(accetp_fd < 0) {
                    printf("accept error.\n");
                    return -1;
                }

                ev.data.fd = accetp_fd;
                ev.events = EPOLLIN; // 水平触发,默认触发方式
                // ev.events = EPOLLIN | EPOLLET; // 边沿触发,需要通过标记 EPOLLET 来指定
                if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, accetp_fd, &ev) < 0) {
                    printf("add fd to epoll error.\n");
                    return -1;
                }
                sleep(2); // 之所以等 2 秒,是为了让客户端发送 1M 的数据发送完毕
            } else {
                ret = recv(accetp_fd, buff, 1024, 0);
                if (ret <= 0) {
                    printf("recv error.\n");
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, accetp_fd, &ev);
                    close(accetp_fd);
                    events[i].data.fd = -1;
                    return -1;
                }
                printf("seq: %d, recv len: %d\n", seq, ret);
                seq++;
            }
        }
    }

    close(listen_fd);
}

 client:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_PORT   (12345)
#define SERVER_IP     "127.0.0.1"
#define MAX_EVENT     (128)
#define MAX_BUFSIZE   (512)

int main(int argc,char *argv[]) {
    int sock_fd;
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(sock_fd < 0) {
        printf("create socket error.\n");
        return -1;
    }

    struct sockaddr_in addr_serv;
    memset(&addr_serv, 0, sizeof(addr_serv));

    addr_serv.sin_family = AF_INET;
    addr_serv.sin_port =  htons(SERVER_PORT);
    addr_serv.sin_addr.s_addr = inet_addr(SERVER_IP);

    if(connect(sock_fd, (struct sockaddr *)&addr_serv,sizeof(struct sockaddr)) < 0){
        printf("connect error.\n");
        return -1;
    }
    char send_buff[1024 * 1024] = "epoll test!";
    int send_total_count = 0;
    int send_tmp_count = 0;
    while (send_total_count < 1024 * 1024) {
        send_tmp_count = send(sock_fd, send_buff, 1024 * 1024, 0);
        send_total_count += send_tmp_count;
    }
    printf("send 1MB success\n");

    close(sock_fd);
    return 0;
}

水平触发测试结果:

计数从 0 开始,到 1023 结束,共接收 1024 次数据,共 1MB

边沿触发测试结果:

第一次接受 1024 的数据之后,后边 epoll_wait() 不会返回事件

 

 3.4 epoll 惊群问题是怎么回事

(1)现象

在 linux 网络方面讨论惊群问题的时候,通常有两个惊群问题,分别是 accept 惊群问题和 epoll 惊群问题。如果多个进程阻塞到同一个 fd 上,当这个 fd 有事件的时候,多个进程会被唤醒,但是只有一个进程获得并处理该事件,导致其它进程空转,进而导致 cpu 资源的浪费。这就是惊群问题。

如果你见过养鸡的,那么很好理解惊群问题。假设养了一群鸡,这个时候你要喂鸡,鸡群看到你来了,都跑了过来,但是这个时候,你手中只有一粒玉米,你往空中一抛,所有的鸡都跳了起来,但是只有一只鸡能吃到这粒玉米,其它集白跳了一次,这就是惊群问题。

从 2.6 开始,linux 内核已经解决了 accept 惊群问题, 具体的解决方式就是给等待队列中的元素增加一个 EXCLUSIVE(排他)标记。

当多个进程阻塞在同一个 listening fd 上时,进程就会被加入到这个 fd 的等待队列中,当 fd 中有事件到来的时候,就会唤醒等待队列中的进程。当没有 EXCLUSIVE 标记的时候,等待队列中的元素全部会被唤醒。

后来给等待元素增加了 EXCLUSIVE 标记,并且带此标记的进程会加到等待队列的尾部。唤醒的时候,从队列头开始唤醒,没有 EXCLUSIVE 标记的进程都会被唤醒,有这个标记的进程只唤醒一个,这样就保证了 EXCLUSIVE 的进程只唤醒一个。

当用户调用 accept(),在内核态会调用到 inet_csk_accept(),如果当前没有新的连接到来,那么就会调用 inet_csk_wait_for_connet(), 进而调用到 prepare_to_wait_exclusive(),从这个函数的名字来看,就是排他性的。所以 accept() 通过 EXCLUSIVE 标志解决了惊群问题。

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
    if (reqsk_queue_empty(queue)) {
        error = inet_csk_wait_for_connect(sk, timeo);
        if (error)
            goto out_err;
    }
}

static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
{
    for (;;) {
        prepare_to_wait_exclusive(sk_sleep(sk), &wait,
                      TASK_INTERRUPTIBLE);
    }
}

(2)原因

由上边分析可知,解决 accept() 惊群问题就是在把进程加入到等待队列时,增加了 EXCLUSIVE 标志,并且唤醒的时候,对于有该标志的进程只唤醒一个。

而在 epoll_wait() 的内核实现中,当没有事件需要等待的时候,也需要把进程加入到等待队列中,加入到等待队列中的时候,也加了 EXCLUSIVE 标志,为什么这种方式下 epoll 仍然有惊群问题呢 ?

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
           int maxevents, struct timespec64 *timeout)
{
    while (1) {
        if (!eavail)
            __add_wait_queue_exclusive(&ep->wq, &wait);
        }
    }
}

可以说,通过 EXCLUSIVE 标志,内核在一定程度上解决了 epoll 惊群问题,但是没有完全解决。

先通过一个例子看一下 epoll 惊群的现象,然后再结合代码分析产生这种现象的原因:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/wait.h>
#include <errno.h>


#define SERVER_IP     ("0.0.0.0")
#define SERVER_PORT   (12345)
#define MAX_LISTENQ   (32)
#define MAX_EVENT     (128)
#define PROCESS_NUM   (10)

int set_fd_nonblocking(int fd){  
    int val = fcntl(fd, F_GETFL);  
    val |= O_NONBLOCK;  
    if(fcntl(fd, F_SETFL, val) < 0){  
        return -1;  
    }  
    return 0;  
}

int set_tcp_server_reuse(int fd) {
    int so_reuse = 1;

    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &so_reuse, sizeof(so_reuse)) < 0) {
        printf("fd[%d] set SO_REUSEADDR error[%s]", fd, strerror(errno));
        return -1;
    }
    return 0;
}

int create_tcp_server() {
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        printf("create socket error: %s\n", strerror(errno));
        return -1;
    }
    set_tcp_server_reuse(listen_fd);

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); /**< 0.0.0.0 all local ip */
    server_addr.sin_port = htons(SERVER_PORT);

    if (bind(listen_fd,(struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        printf("bind[%s:%d] error: %s.\n", SERVER_IP, SERVER_PORT, strerror(errno));
        return -1;
    }

    if (listen(listen_fd, MAX_LISTENQ) < 0) {
        printf("listen error.\n");
        return -1;
    }

    return listen_fd;
}

int main() {
    int ret = -1;
    int accept_fd = -1;
    int event_num = -1;
    int epoll_fd  = -1;
    int listen_fd = -1;

    struct sockaddr_in client_addr;
    struct sockaddr_in server_addr;
    socklen_t client = sizeof(struct sockaddr_in);

    struct epoll_event ev;
    struct epoll_event events[MAX_EVENT];

    listen_fd = create_tcp_server();
    if (listen_fd < 0) {
        printf("create server error\n");
        return -1;
    }
    set_fd_nonblocking(listen_fd);

    epoll_fd = epoll_create(MAX_EVENT);
    if (epoll_fd <= 0) {
        printf("cteare epoll failed, error: %s\n", strerror(errno));
        return -1;
    }

    ev.data.fd = listen_fd;
    ev.events = EPOLLIN;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) < 0) {
        printf("add listen fd to epoll error.\n");
        return -1;
    }

    for(int i = 0; i < PROCESS_NUM; i++){
        int pid = fork();
        if(pid == 0){
            while(1){
                int event_num = 0;
                event_num = epoll_wait(epoll_fd, events, MAX_EVENT, -1);
                printf("process %d return from epoll_wait, event num: %d\n", getpid(), event_num);
                sleep(1);
                for(i = 0; i < event_num; i++){
                    if(!(events[i].events & EPOLLIN)){
                        printf("epoll error\n");
                        return -1;
                    }else if(listen_fd == events[i].data.fd){
                        struct sockaddr in_addr;
                        socklen_t in_len = sizeof(in_addr);
                        accept_fd = accept(listen_fd, &in_addr, &in_len);
                        printf("process %d accept fd %d\n", getpid(), accept_fd);
                        if(accept_fd <= 0){
                            printf("process %d accept failed!\n", getpid());
                        }else{
                            printf("process %d accept successful!\n", getpid());
                        }
                    }
                }
            }
        }
    }
    wait(0);
    return 0;
}

代码解释:

上述代码,首先在主进程中创建一个 epoll fd, 并加入一个 tcp listening fd,然后创建 10 个子进程,这 10 个子进程均对主进程创建的 epoll fd 进行 epoll wait。

设置 socket 选项 SO_REUSEADDR 是为了避免在测试过程中出现 “Address already in use.” 错误。

将 fd 设置为非阻塞,是为了在没有新连接的时候也让 accept() 返回,防止其中一个进程将新连接接收之后,其它进程一直阻塞在 accept() 系统调用,看不到后边的接收成功与否的打印。

测试过程:

上述代码编译之后,在一个终端启动,在另外一个终端通过 telnet 链接监听的端口,telnet 127.0.0.1 12345。

测试现象:

基于上边代码和测试步骤,做三个不同的实验,这三个实验呈现不同的现象。

这三个实验的区别有两方面,分别是监听 fd 是水平触发还是边沿触发, epoll_wait() 返回之后要不要 sleep()。

实验 一:水平触发 + epoll_wait() 返回之后 sleep()

10 个进程都被唤醒,其中一个进程接收了新连接。出现了惊群现象。

实验 二:水平触发 + epoll_wait() 返回之后不进行 sleep() 

被唤醒进程的个数不确定, 范围在 [1, 10] 之间,其中一个进程接收了新连接。出现了惊群现象,但是没有实验 一那么严重。

实验 三:边沿触发 + epoll_wait() 返回之后 sleep()

只有一个进程被唤醒,该进程接收了新链接。没有出现惊群现象。

 

下边通过分析源码,来解释上边三个现象:

在 epoll_wait() 函数中,如果有事件,那么就会唤醒等待进程。进程会调用函数 ep_send_events() 将事件从就绪链表转移到用户态的 event 数组。该函数最后调用了 ep_done_scan(),这个函数最后判断了就绪队列是不是空,如果非空的话,会继续唤醒等待队列中的进程。唤醒一个等待进程之后,当前这个进程就会返回用户态。

static int ep_send_events(struct eventpoll *ep,
              struct epoll_event __user *events, int maxevents)
{
    ep_done_scan(ep, &txlist);
}

static void ep_done_scan(struct eventpoll *ep,
             struct list_head *txlist)
{
    if (!list_empty(&ep->rdllist)) {
        if (waitqueue_active(&ep->wq))
            wake_up(&ep->wq);
    }

    write_unlock_irq(&ep->lock);
}

而后边被唤醒的进程,当被执行的时候,有两种情况:

① 事件已经被之前被唤醒的进程处理,那么该进程检查没有事件,继续睡眠等待,不会返回用户态;

② 事件还没有被处理,那么该进程的工作流程和第一个被唤醒的进程相同,会返回用户态。

根据上边两种情况可以解释实验一和实验二的区别,实验一,在 epoll_wait() 返回之后进行 sleep(),就是为了不让返回的进程立即处理事件,这样就会导致所有的进程都被唤醒,返回用户态;实验二,epoll_wait() 返回之后会立即处理事件,这就和操作系统调度的快慢有关系了,被唤醒的进程个数是不确定的。

当就绪链表中有事件的时候,就会继续唤醒进程,如果是边沿触发的话,也不会惊群了,因为一次之后这个就不会加回就绪链表了。

实验一和实验三之间的区别就是水平触发和边沿触发的区别,因为水平触发的话,事件转移到用户态之后,还会加回就绪队列,这样导致就绪队列不为空,进而导致会继续唤醒进程;而边沿触发不会把事件加回就绪队列,这就绪队列为空,所以不会继续唤醒进程

(3)epoll 惊群如何规避

从上边的分析来看,linux 内核完全解决了 accept() 的惊群问题,因为对于 accept() 来说,一个连接事件是原子的,不能拆分的,只能被一个进程处理,所以内核这么实现是没有问题的,也不会引起用户的异议。

但是 epoll 就不一样,因为 epoll 除了能监听连接事件以外还可以监听接收,发送等事件。就拿接收事件来说,用户可能希望一次接收的数据用多个进程或线程来处理,如果内核强制成只唤醒一个进程,那么就满足不了用户的需求了。

所以说 epoll 惊群,严格意义上来说,并不是问题,而是将最终的决策权交给了用户。

3.5 为什么当发送返回失败时才需要监听 EPOLLOUT 事件

 (1)接收数据和发送数据的区别: 一个被动,一个主动

使用一个连接(tcp socket) 无非就是做两件事情,接收数据和发送数据,

对于接收数据来说,接收方是被动的,只有对端的数据到达本端之后,才会产生 EPOLLIN 事件,之后进行实际数据的接收才是有意义的;

而对于发送来说,发送端是主动的,不是被动的,也就是说只要发送缓冲区有充足的空间,那么发送端就可以发送数据,而不需要有 EPOLLOUT 事件才能发送数据。

所以对于发送数据来说,并不需要一开始就监听 EPOLLOUT 事件,只需要等到这次发送失败(实际发送的数据长度小于要发送的数据长度), 才需要监听发送事件。而一旦 EPOLLOUT 事件上来之后,就需要清除这个事件,不需要一直监听,否则的话每次 epoll wait,只要发送侧有缓冲区可用,都会返回可写事件。

(2)tcp 上报 EPOLLOUT 事件的判断条件

static inline bool __sk_stream_is_writeable(const struct sock *sk, int wake)
{
        // 条件1:剩余空间大于发送缓冲区最大值的 1/3
        // 条件2:沒有发送出去的要小于 lowwat 的临界值
    return sk_stream_wspace(sk) >= sk_stream_min_wspace(sk) &&
           __sk_stream_memory_free(sk, wake);
}

3.6 epoll 相对于 select, poll 有什么区别

(1)epoll 在内核态和用户态,只需要遍历有事件的 fd,不用全部遍历

这个是 epoll 和 select, poll 最明显的别,也是 epoll 最明显的优势。

假如要监听 500 个 fd,而现在只有 10 个 fd 有事件,对于 select 和 poll 来说,当调用 select(), poll() 之后,内核需要把 500 个 fd 遍历一遍,然后返回,返回之后,用户处理事件的时候,仍然要把所有 fd 都遍历一遍,同时处理有事件的 fd。

对于 epoll 来说,当调用 epoll_wait() 之后,在内核态只需要遍历 10 个文件描述符,返回之后,在用户态也只需要处理 10 个事件即可,不需要遍历全部的 500 个描述符。

由此可见,select 和 poll 适用于 fd 个数比较少,或者 fd 个数比较多并且大多数 fd 都比较活跃的场景;而 epoll 适用于 fd 个数比较多,但是活跃 fd 占比不大的场景。

(2)监听的文件个数限制

① select 只能监听 1024 个文件描述符(0 ~ 1023)

select 能监听的 fd 的个数是有限制的,1024 个,并且这 1024 个 fd 的范围也是有限制的,只能是 0~ 1023, 假如你创建了一个 fd 是 2000, 但是被 select 监听的 fd 只有 10 个(没有超过数量限制),那么这个 2000 的 fd 也是不能被 select 监听。因为 fd_set 在内核是一个 bitmap, 用 bitmap 的位来表示一个 fd,所以能表示的 fd 的范围就是 0 ~ 1023。

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

#define __FD_SETSIZE    1024
typedef struct {
    unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

 ② poll 和 epoll 本身没有对文件按描述符个数的限制。

(3)目标事件和实际事件分离

① select 目标事件和实际事件没有分离

select 每次调用之前,都需要将自己要监听的读事件,写事件,错误事件分别设置到对应的 fd_set 中,内核处理的时候,如果要监听的事件,当前没有,就会把对应位置清除,返回给用户态。所以对于 select 来说,用户要监听的事件,以及用户返回的事件,都放到 fd_set 里边,共用这块数据空间。这就导致每次调用 select 时,都需要重新设置一边 fd_set。

② poll 和 epoll 实现了分离

poll 使用一个结构体 strucy poll_fd 来表示一个 被监听的 fd,并且目标事件和实际事件通过 events 和 reevents 来表示,这样对于用户来说不需要每次调用 poll() 之前都设置一遍目标事件。

struct pollfd {
    int fd;
    short int events;
    short int revents;
};

epoll 通过一个专门的 api epoll_ctl 来管理要监听的目标事件,epoll_wait() 的时候直接获取数据事件。 

(4)系统调用个数的区别

select 和 poll 均是只有一个 系统调用,这样使用简单,但是每次都要传递要监听的 fd 以及监听的目标事件,导致在用户态和内核态传递的数据量较大;

epoll 通过三个 api 来实现,epoll_create(), epoll_ctl(), epoll_wait()。epoll 本身也占用一个 fd 资源,select,poll 不存在这种情况,但是 epoll 通过这种方式,将控制面和数据面完全分离,要监听哪些 fd,以及每个 fd 要监听的目标事件,完全在内核进行管理,这就减少了 epoll_wait() 时在用户态和内核态传输的数据量。

  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值