前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布

epoll

原创
作者头像
冰寒火
修改2023-03-07 16:07:56
7830
修改2023-03-07 16:07:56
举报
文章被收录于专栏:软件设计软件设计

前言

io多路复用有很多种实现,自Linux 2.6内核正式引入epoll以来,epoll已经成为了目前实现高性能网络服务器端的必备技术。相比较select、poll而言,在查询、复制、监听数量上,epoll都有极大优势。

select和poll缺陷

1 select

  1. select底层是用一个数组来维护监听的文件描述符,长度默认是1024,有数量的限制。
  2. select采用轮询的方式来检测数组上文件描述符是否准备就绪,如果是就将所有监听的文件描述符返回。
  3. select每次返回给用户线程的连接是监控的所有连接,可能监控的连接有1000个,而活跃的连接只有1个,有效率太低。更适合大部分连接活跃的情况。

2 poll

相比select来说,底层用链表来维护监听的文件描述符,数量没有限制,但是还是采用轮询的方式检测是否准备就绪,存在性能问题。

epoll

1 数据结构

代码语言:c
复制
#include <sys/epoll.h>
//新建epoll描述符
int epoll_create ( int size );
//添加或删除监听的连接
int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );
//返回活跃的连接
int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );

struct epitem
{
    struct rb_node rbn;            //用于主结构管理的红黑树
    struct list_head rdllink;       //事件就绪队列
    struct epitem *next;           //用于主结构体中的链表
    struct epoll_filefd ffd;         //每个fd生成的一个结构
    int nwait;
    struct list_head pwqlist;     //poll等待队列
    struct eventpoll *ep;          //该项属于哪个主结构体
    struct list_head fllink;         //链接fd对应的file链表
    struct epoll_event event;  //注册的感兴趣的事件,也就是用户空间的epoll_event
 }

struct eventpoll
{
    spin_lock_t lock;            //对本数据结构的访问
    struct mutex mtx;            //防止使用时被删除
    wait_queue_head_t wq;        //sys_epoll_wait() 使用的等待队列
    wait_queue_head_t poll_wait; //file->poll()使用的等待队列
    struct list_head rdllist;    //事件满足条件的链表
    struct rb_root rbr;          //用于管理所有fd的红黑树
    struct epitem *ovflist;      //将事件到达的fd进行链接起来发送至用户空间
}

struct epoll_event
{
     __unit32_t events;    // epoll事件
     epoll_data_t data;     // 用户数据 
};

typedef union epoll_data
{
    void* ptr;              //指定与fd相关的用户数据
    int fd;                 //指定事件所从属的目标文件描述符
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

2 原理

与select相比,epoll分清了频繁调用和不频繁调用的操作。例如,epoll_ctl()是不太频繁调用的,而epoll_wait是非常频繁调用的。

epoll中包含红黑树、就绪链表。

红黑树存储监听的套接字,当添加和删除套接字时,都在红黑树上处理。

使用流程

  1. 通过epoll_ctl()添加事件到红黑树上,并在相应Socket上注册回调函数。当有数据到达Socket时,协议栈就会调用这个回调函数:ep_poll_callback(),将这个事件添加到rdlist链表中,并唤醒/通知进程有数据到达。
  2. 当我们调用epoll_wait时只需要检测rdlist上是否有事件,如果没有则挂起这个线程,如果有则将相应的事件复制到events。

3 触发

3.1 水平触发(LT)

如果数据没有读完或者写满,那么这个事件每次都会返回。效率低于ET,尤其是在大流量情况,但是编码简单,不容易出现问题,因为数据没读完,内核会一直通知,不用担心漏掉。

3.2 边缘触发(ET)

如果数据没有读完或者写满,下次不会返回,除非有新数据进入。select、poll只有水平触发。在某一刻有多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪链接,边缘模式只会通知一次,如果accept只处理一个连接,导致TCP剩余连接得不到处理。

边缘模式效率高,尤其是大流量下,会比LT少很多系统调用,但需要考虑数据没读完。

4 回调处理

回调函数是谁执行的?tcp协议内核线程。首先我们看下tcp数据包从网卡驱动到kernel内部tcp协议处理调用链:

当数据到达网卡缓冲后,网卡会发起中断,CPU响应中断,执行设备驱动程序。

CPU响应中断网卡中断后,最后会执行tcp协议栈,将数据包写入到socket buffer,并调用注册的ep_poll_callback。

代码语言:shell
复制
next_rx_action
    |-process_backlog
        ......
            |->packet_type->func 在这里我们考虑ip_rcv
                    |->ipprot->handler 在这里ipprot重载为tcp_protocol
                        (handler 即为tcp_v4_rcv)
tcp_v4_rcv
      |->tcp_v4_do_rcv
            |->tcp_rcv_state_process
                  |->tcp_data_queue
                        |-> sk->sk_data_ready(sock_def_readable)
                              |->wake_up_interruptible_sync_poll(sk->sleep,...)
                                    |->__wake_up
                                          |->__wake_up_common
                                                |->curr->func
                                                /* 这里已经被ep_insert添加为ep_poll_callback,而且设定了排它标识WQ_FLAG_EXCLUSIVE*/
                                                      |->ep_poll_callback
代码语言:shell
复制
wake_up_locked
    |->__wake_up_common
        |->default_wake_function
            |->try_wake_up (wake up a thread)
                |->activate_task
                    |->enqueue_task    running

回调函数中会从红黑树中找到注册事件,添加到就绪队列,然后唤醒因epoll_wait()阻塞的线程。

代码语言:c
复制
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
    // 获取wait对应的epitem
    struct epitem *epi = ep_item_from_wait(wait);
    // epitem对应的eventpoll结构体
    struct eventpoll *ep = epi->ep;
    // 获取自旋锁,保护ready_list等结构
    spin_lock_irqsave(&ep->lock, flags);
    // 如果当前epi没有被链入ep的ready list,则链入
    // 这样,就把当前的可用事件加入到epoll的可用列表了
    if (!ep_is_linked(&epi->rdllink))
        list_add_tail(&epi->rdllink, &ep->rdllist);
    // 如果有epoll_wait在等待的话,则唤醒这个epoll_wait进程
    // 对应的&ep->wq是在epoll_wait调用的时候通过init_waitqueue_entry(&wait, current)而生成的
    // 其中的current即是对应调用epoll_wait的进程信息task_struct
    if (waitqueue_active(&ep->wq))
        wake_up_locked(&ep->wq);
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • select和poll缺陷
    • 1 select
      • 2 poll
      • epoll
        • 1 数据结构
          • 2 原理
            • 3 触发
              • 3.1 水平触发(LT)
              • 3.2 边缘触发(ET)
            • 4 回调处理
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档