「底层原理」epoll源码分析,还搞不懂epoll的看过来
haoteby 2025-09-11 01:10 9 浏览
前言
Linux内核提供了3个关键函数供用户来操作epoll,分别是:
- epoll_create(), 创建eventpoll对象
- epoll_ctl(), 操作eventpoll对象
- epoll_wait(), 从eventpoll对象中返回活跃的事件
而操作系统内部会用到一个名叫epoll_event_callback()的回调函数来调度epoll对象中的事件,这个函数非常重要,故本文将会对上述4个函数进行源码分析。
源码来源
由于epoll的实现内嵌在内核中,直接查看内核源码的话会有一些无关代码影响阅读。为此在GitHub上写的简化版TCP/IP协议栈,里面实现了epoll逻辑。链接为:
https://github.com/wangbojing/NtyTcp
存放着以上4个关键函数的文件是[src\nty_epoll_rb.c],本文接下来通过分析该程序的代码来探索epoll能支持高并发连接的秘密。
两个核心数据结构
(1)epitem
如图所示,epitem是中包含了两个主要的成员变量,分别是rbn和rdlink,前者是红黑树的节点,而后者是双链表的节点,也就是说一个epitem对象即可作为红黑树中的一个节点又可作为双链表中的一个节点。并且每个epitem中存放着一个event,对event的查询也就转换成了对epitem的查询。
struct epitem {
RB_ENTRY(epitem) rbn;
/* RB_ENTRY(epitem) rbn等价于
struct {
struct type *rbe_left; //指向左子树
struct type *rbe_right; //指向右子树
struct type *rbe_parent; //指向父节点
int rbe_color; //该节点的颜色
} rbn
*/
LIST_ENTRY(epitem) rdlink;
/* LIST_ENTRY(epitem) rdlink等价于
struct {
struct type *le_next; //指向下个元素
struct type **le_prev; //前一个元素的地址
}*/
int rdy; //判断该节点是否同时存在与红黑树和双向链表中
int sockfd; //socket句柄
struct epoll_event event; //存放用户填充的事件
};
(2)eventpoll
如图所示,eventpoll中包含了两个主要的成员变量,分别是rbr和rdlist,前者指向红黑树的根节点,后者指向双链表的头结点。即一个eventpoll对象对应二个epitem的容器。对epitem的检索,将发生在这两个容器上(红黑树和双链表)。
struct eventpoll {
/*
struct ep_rb_tree {
struct epitem *rbh_root;
}
*/
ep_rb_tree rbr; //rbr指向红黑树的根节点
int rbcnt; //红黑树中节点的数量(也就是添加了多少个TCP连接事件)
LIST_HEAD( ,epitem) rdlist; //rdlist指向双向链表的头节点;
/* 这个LIST_HEAD等价于
struct {
struct epitem *lh_first;
}rdlist;
*/
int rdnum; //双向链表中节点的数量(也就是有多少个TCP连接来事件了)
// ...略...
};
四个关键函数
(1) epoll_create()
//创建epoll对象,包含一颗空红黑树和一个空双向链表
int epoll_create(int size) {
//与很多内核版本一样,size参数没有作用,只要保证大于0即可
if (size <= 0) return -1;
nty_tcp_manager *tcp = nty_get_tcp_manager(); //获取tcp对象
if (!tcp) return -1;
struct _nty_socket *epsocket = nty_socket_allocate(NTY_TCP_SOCK_EPOLL);
if (epsocket == NULL) {
nty_trace_epoll("malloc failed\n");
return -1;
}
// 1° 开辟了一块内存用于填充eventpoll对象
struct eventpoll *ep = (struct eventpoll*)calloc(1, sizeof(struct eventpoll));
if (!ep) {
nty_free_socket(epsocket->id, 0);
return -1;
}
ep->rbcnt = 0;
// 2° 让红黑树根指向空
RB_INIT(&ep->rbr); //等价于ep->rbr.rbh_root = NULL;
// 3° 让双向链表的头指向空
LIST_INIT(&ep->rdlist); //等价于ep->rdlist.lh_first = NULL;
// 4° 并发环境下进行互斥
// ...该部分代码与主线逻辑无关,可自行查看...
//5° 保存epoll对象
tcp->ep = (void*)ep;
epsocket->ep = (void*)ep;
return epsocket->id;
}
对以上代码的逻辑进行梳理,可以总结为以下6步:
- 创建eventpoll对象
- 让eventpoll中的rbr指向空
- 让eventpoll中的rdlist指向空
- 在并发环境下进行互斥
- 保存eventpoll对象
- 返回eventpoll对象的句柄(id)
相关视频推荐
全网最详细epoll讲解,6种epoll的设计,让你吊打面试官
学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂
需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
(2)epoll_ctl()
该函数的逻辑其实很简单,无非就是将用户传入的参数封装为一个epitem对象,然后根据传入的op是①EPOLL_CTL_ADD、②EPOLL_CTL_MOD还是③EPOLL_CTL_DEL,来决定是①将epitem对象插入红黑树中,②更新红黑树中的epitem对象,还是③移除红黑树中的epitem对象。
//往红黑树中加每个tcp连接以及相关的事件
int epoll_ctl(int epid, int op, int sockid, struct epoll_event *event) {
nty_tcp_manager *tcp = nty_get_tcp_manager();
if (!tcp) return -1;
nty_trace_epoll(" epoll_ctl --> 1111111:%d, sockid:%d\n", epid, sockid);
struct _nty_socket *epsocket = tcp->fdtable->sockfds[epid];
if (epsocket->socktype == NTY_TCP_SOCK_UNUSED) {
errno = -EBADF;
return -1;
}
if (epsocket->socktype != NTY_TCP_SOCK_EPOLL) {
errno = -EINVAL;
return -1;
}
nty_trace_epoll(" epoll_ctl --> eventpoll\n");
struct eventpoll *ep = (struct eventpoll*)epsocket->ep;
if (!ep || (!event && op != EPOLL_CTL_DEL)) {
errno = -EINVAL;
return -1;
}
if (op == EPOLL_CTL_ADD) {
//添加sockfd上关联的事件
pthread_mutex_lock(&ep->mtx);
struct epitem tmp;
tmp.sockfd = sockid;
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp); //先在红黑树上找,根据key来找,也就是这个sockid,找的速度会非常快
if (epi) {
//原来有这个节点,不能再次插入
nty_trace_epoll("rbtree is exist\n");
pthread_mutex_unlock(&ep->mtx);
return -1;
}
//只有红黑树上没有该节点【没有用过EPOLL_CTL_ADD的tcp连接才能走到这里】;
//(1)生成了一个epitem对象,这个结构对象,其实就是红黑的一个节点;
epi = (struct epitem*)calloc(1, sizeof(struct epitem));
if (!epi) {
pthread_mutex_unlock(&ep->mtx);
errno = -ENOMEM;
return -1;
}
//(2)把socket(TCP连接)保存到节点中;
epi->sockfd = sockid; //作为红黑树节点的key,保存在红黑树中
//(3)我们要增加的事件也保存到节点中;
memcpy(&epi->event, event, sizeof(struct epoll_event));
//(4)把这个节点插入到红黑树中去
epi = RB_INSERT(_epoll_rb_socket, &ep->rbr, epi); //实际上这个时候epi的rbn成员就会发挥作用,如果这个红黑树中有多个节点,那么RB_INSERT就会epi->rbi相应的值:可以参考图来理解
assert(epi == NULL);
ep->rbcnt ++;
pthread_mutex_unlock(&ep->mtx);
} else if (op == EPOLL_CTL_DEL) {
pthread_mutex_lock(&ep->mtx);
struct epitem tmp;
tmp.sockfd = sockid;
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);//先在红黑树上找,根据key来找,也就是这个sockid,找的速度会非常快
if (!epi) {
nty_trace_epoll("rbtree no exist\n");
pthread_mutex_unlock(&ep->mtx);
return -1;
}
//只有在红黑树上找到该节点【用过EPOLL_CTL_ADD的tcp连接才能走到这里】;
//从红黑树上把这个节点移除
epi = RB_REMOVE(_epoll_rb_socket, &ep->rbr, epi);
if (!epi) {
nty_trace_epoll("rbtree is no exist\n");
pthread_mutex_unlock(&ep->mtx);
return -1;
}
ep->rbcnt --;
free(epi);
pthread_mutex_unlock(&ep->mtx);
} else if (op == EPOLL_CTL_MOD) {
struct epitem tmp;
tmp.sockfd = sockid;
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp); //先在红黑树上找,根据key来找,也就是这个sockid,找的速度会非常快
if (epi) {
//红黑树上有该节点,则修改对应的事件
epi->event.events = event->events;
epi->event.events |= EPOLLERR | EPOLLHUP;
} else {
errno = -ENOENT;
return -1;
}
} else {
nty_trace_epoll("op is no exist\n");
assert(0);
}
return 0;
}
(3)epoll_wait()
//到双向链表中去取相关的事件通知
int epoll_wait(int epid, struct epoll_event *events, int maxevents, int timeout) {
nty_tcp_manager *tcp = nty_get_tcp_manager();
if (!tcp) return -1;
struct _nty_socket *epsocket = tcp->fdtable->sockfds[epid];
struct eventpoll *ep = (struct eventpoll*)epsocket->ep;
// ...此处主要是一些负责验证性工作的代码...
//(1)当eventpoll对象的双向链表为空时,程序会在这个while中等待一定时间,
//直到有事件被触发,操作系统将epitem插入到双向链表上使得rdnum>0时,程序才会跳出while循环
while (ep->rdnum == 0 && timeout != 0) {
// ...此处主要是一些与等待时间相关的代码...
}
pthread_spin_lock(&ep->lock);
int cnt = 0;
//(1)取得事件的数量
//ep->rdnum:代表双向链表里边的节点数量(也就是有多少个TCP连接来事件了)
//maxevents:此次调用最多可以收集到maxevents个已经就绪【已经准备好】的读写事件
int num = (ep->rdnum > maxevents ? maxevents : ep->rdnum); //哪个数量少,就取得少的数字作为要取的事件数量
int i = 0;
while (num != 0 && !LIST_EMPTY(&ep->rdlist)) { //EPOLLET
//(2)每次都从双向链表头取得 一个一个的节点
struct epitem *epi = LIST_FIRST(&ep->rdlist);
//(3)把这个节点从双向链表中删除【但这并不影响这个节点依旧在红黑树中】
LIST_REMOVE(epi, rdlink);
//(4)这是个标记,标记这个节点【这个节点本身是已经在红黑树中】已经不在双向链表中;
epi->rdy = 0; //当这个节点被操作系统 加入到 双向链表中时,这个标记会设置为1。
//(5)把事件标记信息拷贝出来;拷贝到提供的events参数中
memcpy(&events[i++], &epi->event, sizeof(struct epoll_event));
num --;
cnt ++; //拷贝 出来的 双向链表 中节点数目累加
ep->rdnum --; //双向链表里边的节点数量减1
}
pthread_spin_unlock(&ep->lock);
//(5)返回 实际 发生事件的 tcp连接的数目;
return cnt;
}
该函数的逻辑也十分简单,就是让先看一下eventpoll对象的双链表中是否有节点。如果有节点的话则取出节点中的事件填充到用户传入的指针所指向的内存中。如果没有节点的话,则在while循环中等待一定时间,直到有事件被触发后操作系统会将epitem插入到双向链表上使得rdnum>0时(这个过程是由操作系统调用epoll_event_callback函数完成的),程序才会跳出while循环,去双向链表中取数据。
(4)epoll_event_callback()
通过跟踪epoll_event_callback在内核中被调用的位置。可知,当服务器在以下5种情况会调用epoll_event_callback:
- 客户端connect()连入,服务器处于SYN_RCVD状态时
- 三路握手完成,服务器处于ESTABLISHED状态时
- 客户端close()断开连接,服务器处于FIN_WAIT_1和FIN_WAIT_2状态时
- 客户端send/write()数据,服务器可读时
- 服务器可以发送数据时
接下来,我们来看一下epoll_event_callback的源码:
//当发生客户端三路握手连入、可读、可写、客户端断开等情况时,操作系统会调用这个函数,用以往双向链表中增加一个节点【该节点同时 也在红黑树中】
int epoll_event_callback(struct eventpoll *ep, int sockid, uint32_t event) {
struct epitem tmp;
tmp.sockfd = sockid;
//(1)根据给定的key【这个TCP连接的socket】从红黑树中找到这个节点
struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
if (!epi) {
nty_trace_epoll("rbtree not exist\n");
assert(0);
}
//(2)从红黑树中找到这个节点后,判断这个节点是否已经被连入到双向链表里【判断的是rdy标志】
if (epi->rdy) {
//这个节点已经在双向链表里,那无非是把新发生的事件标志增加到现有的事件标志中
epi->event.events |= event;
return 1;
}
//走到这里,表示 双向链表中并没有这个节点,那要做的就是把这个节点连入到双向链表中
nty_trace_epoll("epoll_event_callback --> %d\n", epi->sockfd);
pthread_spin_lock(&ep->lock);
//(3)标记这个节点已经被放入双向链表中,我们刚才研究epoll_wait()的时候,从双向链表中把这个节点取走的时候,这个标志被设置回了0
epi->rdy = 1;
//(4)把这个节点链入到双向链表的表头位置
LIST_INSERT_HEAD(&ep->rdlist, epi, rdlink);
//(5)双向链表中的节点数量加1,刚才研究epoll_wait()的时候,从双向链表中把这个节点取走的时候,这个数量减了1
ep->rdnum ++;
pthread_spin_unlock(&ep->lock);
pthread_mutex_lock(&ep->cdmtx);
pthread_cond_signal(&ep->cond);
pthread_mutex_unlock(&ep->cdmtx);
return 0;
}
以上代码的逻辑也十分简单,就是将eventpoll所指向的红黑树的节点插入到双向链表中。
总结
epoll底层实现中有两个关键的数据结构,一个是eventpoll另一个是epitem,其中eventpoll中有两个成员变量分别是rbr和rdlist,前者指向一颗红黑树的根,后者指向双向链表的头。而epitem则是红黑树节点和双向链表节点的综合体,也就是说epitem即可作为树的节点,又可以作为链表的节点,并且epitem中包含着用户注册的事件。
- 当用户调用epoll_create()时,会创建eventpoll对象(包含一个红黑树和一个双链表);
- 而用户调用epoll_ctl(ADD)时,会在红黑树上增加节点(epitem对象);
- 接下来,操作系统会默默地在通过epoll_event_callback()来管理eventpoll对象。当有事件被触发时,操作系统则会调用epoll_event_callback函数,将含有该事件的epitem添加到双向链表中。
- 当用户需要管理连接时,只需通过epoll_wait()从eventpoll对象中的双链表下"摘取"epitem并取出其包含的事件即可。
相关推荐
- 如何随时清理浏览器缓存_清理浏览器缓存怎么弄
-
想随时清理浏览器缓存吗?Cookieformac版是Macos上一款浏览器缓存清理工具,所有的浏览器Cookie,本地存储数据,HTML5数据库,FlashCookie,Silverlight,...
- Luminati代理动态IP教程指南配置代理VMLogin中文版反指纹浏览器
-
介绍如何使用在VMLogin中文版设置Luminati代理。首先下载VMLogin中文版反指纹浏览器(https://cn.vmlogin.com)对于刚接触Luminati动态ip的朋友,是不是不懂...
- mac清除工具分享,解除您在安全方面的后顾之忧
-
想要永久的安全的处理掉重要数据,删除是之一,使用今天小编分享的mac清除工具,为您的操作再增一层“保护”,小伙伴慎用哟,一旦使用就不可以恢复咯,来吧一起看看吧~mac清除工具分享,解除您在安全方面的后...
- 取代cookie的网站追踪技术:”帆布指纹识别”
-
【前言】一般情况下,网站或者广告联盟都会非常想要一种技术方式可以在网络上精确定位到每一个个体,这样可以通过收集这些个体的数据,通过分析后更加精准的去推送广告(精准化营销)或其他有针对性的一些活动。Co...
- 辅助上网为啥会被抛弃 曲奇(Cookie)虽甜但有毒
-
近期有个小新闻,大概很多小伙伴都没有注意到,那就是谷歌Chrome浏览器要弃用Cookie了!说到Cookie功能,很多小伙伴大概觉得不怎么熟悉,有可能还不如前一段时间被弃用的Flash“出名”,但它...
- 浏览器指纹是什么?浏览器指纹包括哪些信息
-
本文关键词:浏览器指纹、指纹浏览器、浏览器指纹信息、指纹浏览器原理什么是浏览器指纹?浏览器指纹是指浏览器的各种信息,当我们访问其他网站时,即使是在匿名的模式下,这些信息也可以帮助网站识别我们的身份。...
- 那些通用清除软件不曾注意的秘密_清理不常用的应用软件
-
系统清理就像卫生检查前的大扫除,即使你使出吃奶的劲儿把一切可能的地方都打扫过,还会留下边边角角的遗漏。随着大家电脑安全意识的提高,越来越多的朋友开始关注自己的电脑安全,也知道安装360系列软件来"武装...
- 「网络安全宣传周」这些安全上网小知识你要知道!
-
小布说:互联网改变了人们的衣食住行,但与之伴生的网络安全威胁也不容忽视。近些年来,风靡全球的勒索病毒、时有发生的电信诈骗、防不胜防的个人信息泄露时时刻刻都威胁着我们的生活。9月18日-24日是第四届...
- TypeScript 终极初学者指南_typescript 进阶
-
在过去的几年里TypeScript变得越来越流行,现在许多工作都要求开发人员了解TypeScript...
- jQuery知识一览_jquery的认识和使用
-
一、概览jQuery官网:https://jquery.com/jQuery是一个高效、轻量并且功能丰富的js库。核心在于查询query。...
- 我的第一个Electron应用_electronmy
-
hello,好久不见,最近笔者花了几天时间入门Electron,然后做了一个非常简单的应用,本文就来给各位分享一下过程,Electron大佬请随意~笔者开源了一个Web思维导图,虽然借助showSav...
- HTML5 之拖放(Drag 和 Drop)_html拖放api
-
简介拖放是一种常见的特性,即抓取对象以后拖到另一个位置。在HTML5中,拖放是标准的一部分,任何元素都能够拖放。先点击一个小例子:在用户开始拖动<p>元素时执行JavaScrip...
- 如何用JavaScript判断输入值是数字还是字母?
-
在日常开发中,我们有时候需要判断用户输入的是数字还是字母。本文将介绍如何用JavaScript实现这一功能。检查输入值是否是数字或字母...
- 图形编辑器开发:快捷键的管理_图形编辑工具
-
大家好,我是前端西瓜哥。...
- 浏览器原生剪贴板:原来它能这样读取用户截图!
-
当我们使用GitHub时,会发现Ctrl+V就能直接读取用户剪贴板图片进行粘贴,那么它是如何工作的?安全性如何?...