0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

epoll源码分析

科技绿洲 来源:Linux开发架构之路 作者:Linux开发架构之路 2023-11-13 11:49 次阅读

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个关键函数的文件是[srcnty_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 failedn");
		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步:

  1. 创建eventpoll对象
  2. 让eventpoll中的rbr指向空
  3. 让eventpoll中的rdlist指向空
  4. 在并发环境下进行互斥
  5. 保存eventpoll对象
  6. 返回eventpoll对象的句柄(id)

(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:%dn", 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 -- > eventpolln");

	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 existn");
			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 existn");
			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 existn");
			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 existn");
		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:

  1. 客户端connect()连入,服务器处于SYN_RCVD状态时
  2. 三路握手完成,服务器处于ESTABLISHED状态时
  3. 客户端close()断开连接,服务器处于FIN_WAIT_1和FIN_WAIT_2状态时
  4. 客户端send/write()数据,服务器可读时
  5. 服务器可以发送数据时

接下来,我们来看一下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 existn");
		assert(0);
	}

	//(2)从红黑树中找到这个节点后,判断这个节点是否已经被连入到双向链表里【判断的是rdy标志】
	if (epi- >rdy) {
		//这个节点已经在双向链表里,那无非是把新发生的事件标志增加到现有的事件标志中
		epi- >event.events |= event;
		return 1;
	} 

	//走到这里,表示 双向链表中并没有这个节点,那要做的就是把这个节点连入到双向链表中

	nty_trace_epoll("epoll_event_callback -- > %dn", 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并取出其包含的事件即可。
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 函数
    +关注

    关注

    3

    文章

    3868

    浏览量

    61309
  • 代码
    +关注

    关注

    30

    文章

    4555

    浏览量

    66767
  • 协议栈
    +关注

    关注

    2

    文章

    129

    浏览量

    33456
  • epoll
    +关注

    关注

    0

    文章

    28

    浏览量

    2912
收藏 人收藏

    评论

    相关推荐

    epoll的使用

    以下内容是参考华清远见《linux/unix系统编程手册》对epoll的一个个人总结,是我在华清远见比较全面的总结。一、epoll的优点同I/O多路复用和信号驱动I/O一样,linux的epoll
    发表于 05-11 13:22

    我读过的最好的epoll讲解

    的select以及epoll)处理甚至直接忽略。 为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害
    发表于 05-12 15:30

    epoll使用方法与poll的区别

    因为epoll的触发机制是在内核中直接完成整个功能 那个事件准备就绪我就直接返回这个IO事件
    发表于 07-31 10:03

    epoll_wait的事件返回的fd为错误是怎么回事?

    netlink 的 socket 连接 的 fd 为18,但是添加到epollepoll_wait()返回的fd 为 0为什么会出现这样的现象?补充 说明:1、 epoll_wait返回
    发表于 06-12 09:03

    揭示EPOLL一些原理性的东西

    事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样
    发表于 08-24 16:32

    【米尔王牌产品MYD-Y6ULX-V2开发板试用体验】socket通信和epoll

    ;gt;#include <sys/epoll.h>#include "ssd1306.h"const int PORT = 8888
    发表于 11-10 15:31

    uboot源码分析,思路还算清晰

    uboot源码分析,思路还算清晰
    发表于 10-24 15:25 19次下载
    uboot<b class='flag-5'>源码</b><b class='flag-5'>分析</b>,思路还算清晰

    关于Epoll,你应该知道的那些细节

    Epoll,位于头文件sys/epoll.h,是Linux系统上的I/O事件通知基础设施。epoll API为Linux系统专有,于内核2.5.44中首次引入,glibc于2.3.2版本加入支持。其它提供类似的功能的系统,包括F
    发表于 05-12 09:25 1027次阅读

    poll&&epollepoll实现

    poll&&epollepoll实现
    发表于 05-14 14:34 2643次阅读
    poll&&<b class='flag-5'>epoll</b>之<b class='flag-5'>epoll</b>实现

    Linux中epoll IO多路复用机制

    epoll 是Linux内核中的一种可扩展IO事件处理机制,最早在 Linux 2.5.44内核中引入,可被用于代替POSIX select 和 poll 系统调用,并且在具有大量应用程序请求时能够
    发表于 05-16 16:07 589次阅读
    Linux中<b class='flag-5'>epoll</b> IO多路复用机制

    一文详解epoll的实现原理

    本文以四个方面介绍epoll的实现原理,1.epoll的数据结构;2.协议栈如何与epoll通信;3.epoll线程安全如何加锁;4.ET与LT的实现。
    的头像 发表于 08-01 13:28 3466次阅读

    epoll来实现多路复用

    本人用epoll来实现多路复用,epoll触发模式有两种: ET(边缘模式) LT(水平模式) LT模式 是标准模式,意味着每次epoll_wait()返回后,事件处理后,如果之后还有数据,会不断
    的头像 发表于 11-09 10:15 215次阅读
    用<b class='flag-5'>epoll</b>来实现多路复用

    epoll 的实现原理

    今儿我们就从源码入手,来帮助大家简单理解一下 epoll 的实现原理,并在后边分析一下,大家都说 epoll 性能好,那到底是好在哪里。 epoll
    的头像 发表于 11-09 11:14 236次阅读
    <b class='flag-5'>epoll</b> 的实现原理

    epoll的基础数据结构

    一、epoll的基础数据结构 在开始研究源代码之前,我们先看一下 epoll 中使用的数据结构,分别是 eventpoll、epitem 和 eppoll_entry。 1、eventpoll 我们
    的头像 发表于 11-10 10:20 333次阅读
    <b class='flag-5'>epoll</b>的基础数据结构

    epoll底层如何使用红黑树

    epoll和poll的一个很大的区别在于,poll每次调用时都会存在一个将pollfd结构体数组中的每个结构体元素从用户态向内核态中的一个链表节点拷贝的过程,而内核中的这个链表并不会一直保存
    的头像 发表于 11-10 15:13 269次阅读
    <b class='flag-5'>epoll</b>底层如何使用红黑树