深入剖析linux内核epoll源码与原理

引言:

Epoll 是 Linux IO 多路复用的管理机制。作为现在 Linux 平台高性能网络 IO 必要的组件。内核的实现可以参照Linux内核下的:fs/eventpoll.c。 epoll的重要性和使用方法在我之前的文章中也谈过,感兴趣的朋友可以去看看 《深入剖析linux 网络io多路复用》,网址:深入剖析linux 网络io多路复用_全宇宙最帅的程序员的博客-CSDN博客

突然兴起,想要研究一下epoll内核代码实现,接下来把我的学习成果与大家分享。

内核的 epoll 可以从四方面来理解:

 1. epoll 的数据结构,rbtree 对的存储,ready 队列存储就绪 io;

 2. epoll 的线程安全,SMP 的运行,以及防止死锁;

3. epoll 内核回调;

4. epoll 的 LT(水平触发)与 ET(边沿触发)。 下面从这四个方面来谈谈epoll:

一. epoll 数据结构 

epoll给调用者提供三个接口:

 int epoll_create(int size)

  • 参数size指定了epoll实例所能处理的最大文件描述符数目,但实际上这个参数已经不再起作用,通常可以将其设置为任意值。
  • 返回值是一个非负整数,称为epoll实例的文件描述符,用于后续的操作。

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

  • 参数epfdepoll实例的文件描述符。
  • 参数op表示要执行的操作类型,可以是以下三种之一:
    • EPOLL_CTL_ADD:向epoll实例中添加新的文件描述符。
    • EPOLL_CTL_MOD:修改已有文件描述符的监听事件。
    • EPOLL_CTL_DEL:从epoll实例中删除文件描述符。
  • 参数fd是要进行操作的文件描述符。
  • 参数event是一个指向struct epoll_event结构体的指针,用于指定相关的事件和数据。
  • 返回值为0表示操作成功,-1表示操作失败。

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

  • 参数epfdepoll实例的文件描述符。
  • 参数events是一个指向struct epoll_event结构体数组的指针,用于接收就绪的事件。
  • 参数maxevents表示最多可以接收的事件数目。
  • 参数timeout表示等待的超时时间,单位为毫秒。如果设置为负数,则表示永久等待直到有事件到来。
  • 返回值为就绪事件的数目,如果返回值为0,表示超时但没有事件到来,-1表示出现错误。

那么,这个三个函数时是怎么实现和为什么怎么实现呢?

当调用者使用epoll_create去创建一个epoll的时候,内核会初始化一个结构体:eventpoll;

该结构体如下:

struct eventpoll{

    ep_rb_tree rbr; //epoll中的红黑树
    LIST_HEAD(,epitem)  rdlist //epoll中的就绪链表
    
    int rdnum;    //处于就绪状态的事件数量

    int waiting;   //正在等待的处理事件的线程数量

    pthread_mutex_t mtx;        //锁住红黑树
    pthread_spinlock_t lock;    // 锁住就绪链表

    pthread_cond_t cond;        //通知和阻塞事件

    pthread_mutex_t cdmtx      //给condition加锁

}

当调用者使用epoll_ctl去操作epoll中的事件时:内核又会初始化一个结构体:epitem;

该结构体用于表示一个文件描述符及其关联的事件,在epoll实例中维护文件描述符和事件之间的对应关系,包括需要监控的事件、当前状态等信息。

该结构体如下:

struct epitem {
    RB_ENTRY(epitem) rbn;     //该事件在红黑树中的节点
    LIST_ENTRY(epitem) rblink; //该事件在双向链表中的节点
    int rdy;                  //标志位,该事件就绪时,rdy为1,未就绪时,为0

    int socket;             //该事件对应的文件socketfd
    struct epoll_event event;  /*
epoll_event:监控的事件类型和用户数据。它是struct epoll_event类型的结构体
,用于描述需要监控的事件及用户自定义的数据。
该结构体包括两个对象:
1. events:表示监控的事件类型,可包含多个事件类型。
2 .data:用户自定义的数据,这些数据与文件描述符关联,
当相应的事件就绪时,将传递给用户程序。
*/
}

用一个形象的图表示这些数据结构:

最上面的list 用来存储准备就绪的 IO。对于数据结构主要讨论两方面:insert和remove;

何时将数据插入到 rdlist 中呢?当内核 IO 准备就绪的时候,则会执行 epoll_event_callback 的回调函数,将 epitem添加到 list 中。 那何时删除 list 中的数据呢?当 epoll_wait 激活重新运行的时候,将 rdlist 的 epitem 逐一 copy 到 events 参数中。 rbtree 用来存储所有 io 的数据,方便快速通过io_fd 查找. 对于 rbtree 何时添加:执行 epoll_ctl  EPOLL_CTL_ADD 操作,将 epitem 添加到 rbtree 中。何时删除呢?执行 epoll_ctl  EPOLL_CTL_DEL 操作,将 epitem 从rbtree 中删除。 list 与 rbtree 的操作又如何做到线程安全?

二.  epoll锁机制

 rdlist 的操作,rbtree 的操作,epoll_wait 的等待。 rdist 使用最小粒度的锁 spinlock,便于在多线程下,添加操作的时候,能够快速操作rdlist:

1. rdlist的添加:


pthread_spin_lock(&ep->lock);  //获取 spinlock。
ep->rdy=1;       //epitem 的 rdy 置为 1,代表 epitem 已经在就绪队列中,后续再触发相同事件就只需更改 event
LIST_INSERT_HEAD(&ep->rdlist,epi,rdlink); //添加到rdlist中
ep->rdnum++;  //将eventpoll 的rdnum域加1
pthread_spin_unlock(&ep->lock); //释放 spinlock

2. rdlist的删除:

pthread_spin_lock(&ep->lock);
int cnt = 0;
int num =(ep->rdnum > maxevents ? maxevents : ep->rdnum); //判读 rdnum与maxevents的大小,避免 event 溢出 
int i = 0;
while (num != O && !LIST_EMPTY(&ep->rdlist)){ //循环遍历 rdlist,判断添加 rdlist 不能为空
struct epitem *epi = LIST_FIRST(&ep->rdlist);  //获取 rdlist 首个结点
LIST_REMOVE(epi, rdlink); //移除 rdlist 首个结点
epi->rdy =0; //将 epitem 的 rdy 域置为 0,标识 epitem 不再就绪队列中
memcpy(&events[i++], &epi->event, sizeof(struct epoll_event));//copy epitem 的 event 到用户空间的 events
num --; 
cnt ++;
ep->rdnum --;
}

pthread_spin_unlock(&ep->lock);

3.rbtree的添加:

pthread mutex lock(&ep->mtx); //获取互斥锁

struct epitem tmp;
tmp.sockfd = sockid;
struct epitem *epi = RB_FIND( epoll rb socket, &ep->rbr, &tmp);//查找 sockid 的 epitem 是否存在。存在则不能添加,不存在则可以添加
if (epi){
    pthread mutex unlock(&ep->mtx);
    return -1;
}
epi = (struct epitem*)calloc(1, sizeof(struct epitem));
if (!epi) {
    pthread mutex unlock(&ep->mtx);
    errno = -ENOMEM:
    return -1;
}
epi->sockfd = sockid;
memcpy(&epi->event, event, sizeof(struct epoll event));  //将设置的 event 添加到 epitem 的 event 域
epi = RB_INSERT( epoll rb socket,&ep->rbr, epi);
assert(epi == NULL);
pthread mutex unlock(&ep->mtx);

4.rbtree的删除:

pthread mutex_lock(&ep->mtx);  //获取互斥锁
struct epitem tmp;
tmp .sockfd = sockid;

//删除 sockid 的结点,如果不存在,则 rbtree 返回-1
struct epitem *epi = RB_REMOVE(_epoll_rb_socket, &ep->rbr, &tmp);
if (!epi) {
   pthread mutex unlock(&ep->mtx);
   return -1;
}
free(epi);
pthread mutex unlock(&ep->mtx);

三:epoll_wait()的挂起状态, epoll_callback()这两者的机制相对复杂,我将在以后的文章中,单独阐述。

四:epoll的水平触发(LT)和边缘触发(ET):

LT(水平触发)与 ET(边沿触发)是电子信号里面的概念。不清楚可以 man epoll 查看的。 如下图所示:

比如:event = EPOLLIN | EPOLLLT,将 event 设置为 EPOLLIN 与水平触发。只要 event 为 EPOLLIN 时就能不断调用 epoll 回调函数。 比如: event = EPOLLIN | EPOLLET,event 如果从 EPOLLOUT 变化为 EPOLLIN 的时候,就会触 发。在此情形下,变化只发生一次,故只调用一次 epoll 回调函数。关于水平触发与边沿触 发放在 epoll 回调函数执行的时候,如果为 EPOLLET(边沿触发),与之前的 event 对比,如 果发生改变则调用 epoll 回调函数,如果为 EPOLLLT(水平触发),则查看 event 是否为 EPOLLIN, 即可调用 epoll 回调函数。

  1. 水平触发(Level-Triggered)模式:

在水平触发模式下,当 I/O 事件就绪时,epoll_wait 将立即返回该事件,并且一直保持就绪状态,直到应用程序处理完该事件。如果应用程序没有读取所有可用的数据或没有执行相应的操作,epoll_wait 会一直返回该事件。

特点:

  • 直到事件被处理完成之前,内核会持续不断地通知应用程序该事件的状态。
  • 应用程序需要及时处理事件并执行相应的操作,否则可能导致资源浪费。
  • 因为事件在就绪时一直保持,所以应用程序可以多次调用 epoll_wait 来获取事件状态。
  1. 边缘触发(Edge-Triggered)模式:

在边缘触发模式下,当 I/O 事件就绪时,epoll_wait 只会通知一次该事件,即使应用程序没有处理完该事件,也不会再次通知。

特点:

  • epoll_wait 只通知应用程序一次事件就绪,不会重复通知。
  • 如果应用程序没有及时处理事件,可能会错过一些事件。
  • 应用程序需要确保在每次 epoll_wait 返回后,处理完所有就绪事件,防止事件丢失。

边缘触发模式相比水平触发模式,可以减少事件通知的次数,降低系统资源消耗。但它也要求应用程序能够高效地处理事件,以免错过事件。

为了使用边缘触发模式,需要通过 epoll_ctl 函数将关联的文件描述符设置为边缘触发模式。

总结:

  • 水平触发模式下,事件就绪后会持续通知,直到应用程序处理完成。
  • 边缘触发模式下,事件就绪只会通知一次,应用程序需要确保及时处理事件,以免错过。
  • 边缘触发模式相对于水平触发模式,可以减少事件通知次数和系统开销,但要求应用程序能够高效处理事件。

-------------------------------------------------------------------------------------------------------------------end

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值