epoll为什么优于select

select 和 epoll 是 linux 中典型的多路复用模型,用于绑定监听多个事件源(实现了poll接口的文件描述符?),若无事件则该进程处于当阻塞状态或一定时间后超时,若某个事件源的某个动作触发(数据拷贝完成),多路复用模型将进程加入工作队列,进程根据处理事件。众所周知 epoll 是比 select 更先进的多路复用模型本文不求甚解的解释此事。并不会涉及epoll的具体实现细节(数据结构)。

select 实现

select 调用如下:

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

使用select监控多个链接的代码大致如下:

#define MAXCLINE 5       // 连接队列中的个数
int fd[MAXCLINE];        // 连接的文件描述符队列

int main(void)
{
      sock_fd = socket(AF_INET,SOCK_STREAM,0)          // 建立主机间通信的 socket 结构体
      .....
      bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr);         // 绑定socket到当前服务器
      listen(sock_fd, 5);  // 监听 5 个TCP连接

      fd_set fdsr;         // bitmap类型的文件描述符集合,01100 表示第1、2位有数据到达
      int max;

      for(i = 0; i < 5; i++)
      {
          .....
          fd[i] = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);   // 跟 5 个客户端依次建立 TCP 连接,并将连接放入 fd 文件描述符队列
      }

      while(1)               // 循环监听连接上的数据是否到达
      {
        FD_ZERO(&fdsr);      // 对 fd_set 即 bitmap 类型进行复位,即全部重置为0

        for(i = 0; i < 5; i++)
        {
             FD_SET(fd[i], &fdsr);      // 将要监听的TCP连接对应的文件描述符所在的bitmap的位置置1,比如 0110010110 表示需要监听第 1、2、5、7、8个文件描述符对应的 TCP 连接
        }

        ret = select(max + 1, &fdsr, NULL, NULL, NULL);  // 调用select系统函数进入内核检查哪个连接的数据到达

        for(i=0;i<5;i++)
        {
            if(FD_ISSET(fd[i], &fdsr))      // fd_set中为1的位置表示的连接,意味着有数据到达,可以让用户进程读取
            {
                ret = recv(fd[i], buf,sizeof(buf), 0);
                ......
            }
        }
  }

1)调用select 后将本进程加入所有socket的等待队列中;

img

2)当socket 收到数据,中断控制程序将进程从所有的socket的等待队列中移除,进程加入工作队列。

img

3)进程遍历自己监控的socket 列表找到有数据的socket 进行处理。

当进程调用 select 时,内核会执行以下操作:

(1) 进程阻塞

  • 用户态调用 select 后,进程从用户态陷入内核态。

  • 内核检查所有监听的文件描述符(fdsr中设为1的位)的接收缓冲区:

    • 如果有文件描述符已就绪(数据到达):

      • 内核直接返回,select 不会阻塞。
    • 如果无文件描述符就绪:

      • 内核将当前进程的 task_struct 加入每个监听文件描述符的等待队列(wait queue)。
  • 进程进入 TASK_INTERRUPTIBLE 状态(可中断睡眠),主动让出 CPU。

(2) 内核唤醒进程

当任意一个被监听的文件描述符满足条件时(例如数据到达):

  1. 网络协议栈的触发:

    • 数据包到达网卡后,内核协议栈处理数据并填充到对应 Socket 的接收缓冲区。
    • 内核调用 Socket 的唤醒回调函数(例如 sock_def_readable)。
  2. 等待队列的唤醒:

    • 内核遍历该 Socket 的等待队列,找到所有监听该文件描述符的进程(例如你的进程)。
    • 将进程状态从 TASK_INTERRUPTIBLE 改为 TASK_RUNNING,并重新加入 CPU 调度队列。
  3. 进程重新运行:

    • 进程被调度器选中后,继续执行 select 的内核逻辑。
  • 内核再次检查文件描述符的就绪状态,更新 fdsr 位图,然后返回到用户态。

(3) 用户态继续执行

  • select 返回后,用户代码通过 FD_ISSET 检查哪些文件描述符有数据到达。
  • 进程调用 recv 读取数据(此时不会阻塞,因为内核已确认数据就绪)。

select 缺陷

1.性能开销大

1)调用 select 时会陷入内核,这时需要将参数中的 fd_set 从用户空间拷贝到内核空间,select 执行完后,还需要将 fd_set 从内核空间拷贝回用户空间,高并发场景下这样的拷贝会消耗极大资源;(epoll 优化为不拷贝)

2)进程被唤醒后,不知道哪些连接已就绪即收到了数据,需要遍历传递进来的所有 fd_set 的每一位,不管它们是否就绪;(epoll 优化为异步事件通知)

3)select 只返回就绪文件的个数,具体哪个文件可读还需要遍历;(epoll 优化为只返回就绪的文件描述符,无需做无效的遍历)

2.同时能够监听的文件描述符数量太少。受限于 sizeof(fd_set) 的大小,在编译内核时就确定了且无法更改。一般是 32 位操作系统是 1024,64 位是 2048。(poll、epoll 优化为适应链表方式)

epoll

select 仅一个方法,epoll 有三个方法

int epoll_create(int size);   // 创建一个 eventpoll 内核对象
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);   // 将连接到socket对象添加到 eventpoll 对象上,epoll_event是要监听的事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);      // 等待连接 socket 的数据是否到达

具体使用方法如下:

int main(void)
{
      struct epoll_event events[5];
      int epfd = epoll_create(10);         // 创建一个 epoll 对象
      ......
      for(i = 0; i < 5; i++)
      {
          static struct epoll_event ev;
          .....
          ev.data.fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);
          ev.events = EPOLLIN;
          epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);  // 向 epoll 对象中添加要管理的连接
      }

      while(1)
      {
         nfds = epoll_wait(epfd, events, 5, 10000);   // 等待其管理的连接上的 IO 事件

         for(i=0; i<nfds; i++)
         {
             ......
             read(events[i].data.fd, buff, MAXBUF)
         }
  }

先用epoll_create创建一个epoll对象epfd,再通过epoll_ctl将需要监视的socket添加到epfd中,最后调用epoll_wait等待数据。

改进1 功能分离

select低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。每次调用select都需要这两步操作,然而大多数应用场景中,需要监视的socket相对固定,并不需要每次都修改。epoll将这两个操作分开,先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程。显而易见的,效率就能得到提升。

改进2 就绪列表

select低效的另一个原因在于程序不知道哪些socket收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的socket,就能避免遍历。当进程被唤醒后,只要获取rdlist的内容,就能够知道哪些socket收到数据。当然这个就绪列表是通过红黑树维护的。

epoll实现

1)当epoll_create 会在内核中创建一个eventpoll 对象,其中包含了该进程描述符和回调函数(将进程修改为运行态),

2)epoll_ctl 会将 socket 连接注册到 eventpoll 对象,将epoll的回调函数(与前面的并非同一个,用于根据事件触发 epoll 后续行为)添加到 socket 的进程等待队列中,

3)epoll_wait会将当前进程加入等待队列,socket 的数据接收队列有数据到达时将该socket 对应的 epitem 添加到 eventpoll 对象的就绪队列 rdllist 中, 然后查看 eventpoll 对象的进程等待队列上是否有等待项,通过回调函数 default_wake_func 唤醒这个进程,进行数据的处理;

epoll_create

epoll_create 函数会创建一个 struct eventpoll 的内核对象,类似 socket,把它关联到当前进程的已打开文件列表中。

eventpoll 主要包含三个字段:

struct eventpoll {
 wait_queue_head_t wq;      // 等待队列链表,存放阻塞的进程

 struct list_head rdllist;  // 数据就绪的文件描述符都会放到这里

 struct rb_root rbr;        // 红黑树,管理用户进程下添加进来的所有 socket 连接
        ......
}

wq:等待队列,如果当前进程没有数据需要处理,会把当前进程描述符和回调函数 default_wake_functon 构造一个等待队列项,放入当前 wq 对待队列,软中断数据就绪的时候会通过 wq 来找到阻塞在 epoll 对象上的用户进程。

rbr:一棵红黑树,管理用户进程下添加进来的所有 socket 连接。

rdllist:就绪的描述符的链表。当有的连接数据就绪的时候,内核会把就绪的连接放到 rdllist 链表里。这样应用进程只需要判断链表就能找出就绪进程,而不用去遍历整棵树。

img

epoll_ctl

epoll_ctl 函数主要负责把服务端和客户端建立的 socket 连接注册到 eventpoll 对象里,会做三件事:

1)创建一个 epitem 对象,主要包含两个字段,分别存放 socket fd 即连接的文件描述符,和所属的 eventpoll 对象的指针;

2)将一个数据到达时用到的回调函数添加到 socket 的进程等待队列中,这里添加的 socket 的进程等待队列结构中,只有回调函数,没有设置进程描述符,因为在 epoll 中,进程是放在 eventpoll 的等待队列中,等待被 epoll_wait 函数唤醒,而不是放在 socket 的进程等待队列中;

3)将第 1)步创建的 epitem 对象插入红黑树;

epoll_wait

epoll_wait 函数的动作比较简单,检查 eventpoll 对象的就绪的连接 rdllist 上是否有数据到达,如果没有就把当前的进程描述符添加到一个等待队列项里,加入到 eventpoll 的进程等待队列里,然后阻塞当前进程,等待数据到达时通过回调函数被唤醒。

当 eventpoll 监控的连接上有数据到达时,通过下面几个步骤唤醒对应的进程处理数据:

1)socket 的数据接收队列有数据到达,会通过进程等待队列的回调函数 ep_poll_callback 唤醒红黑树中的节点 epitem;

2)ep_poll_callback 函数将有数据到达的 epitem 添加到 eventpoll 对象的就绪队列 rdllist 中;

3)ep_poll_callback 函数检查 eventpoll 对象的进程等待队列上是否有等待项,通过回调函数 default_wake_func 唤醒这个进程,进行数据的处理;

4)当进程醒来后,继续从 epoll_wait 时暂停的代码继续执行,把 rdlist 中就绪的事件返回给用户进程,让用户进程调用 recv 把已经到达内核 socket 等待队列的数据拷贝到用户空间使用。

img

三种多路复用模型的特点

img

参考:

https://www.cnblogs.com/looyee/articles/12964911.html

https://cloud.tencent.com/developer/article/2188691

https://blog.csdn.net/JMW1407/article/details/107963618

updatedupdated2025-07-022025-07-02