百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

epoll源码剖析:为什么使用红黑树以及如何使用红黑树

gudong366 2025-05-16 16:16 9 浏览


linux服务器开发相关视频解析 :

90分钟了解4种红黑树的Linux内核应用场景

linux下的epoll实战揭秘——支撑亿级IO的底层基石

我们知道epoll的底层使用了红黑树来管理文件描述符,为什么会选择红黑树这种结构呢?

以下是个人理解:

epoll和poll的一个很大的区别在于,poll每次调用时都会存在一个将pollfd结构体数组中的每个结构体元素从用户态向内核态中的一个链表节点拷贝的过程,而内核中的这个链表并不会一直保存,当poll运行一次就会重新执行一次上述的拷贝过程,这说明一个问题:poll并不会在内核中为要监听的文件描述符长久的维护一个数据结构来存放他们,而epoll内核中维护了一个内核事件表,它是将所有的文件描述符全部都存放在内核中,系统去检测有事件发生的时候触发回调,当你要添加新的文件描述符的时候也是调用epoll_ctl函数使用EPOLL_CTL_ADD宏来插入,epoll_wait也不是每次调用时都会重新拷贝一遍所有的文件描述符到内核态。当我现在要在内核中长久的维护一个数据结构来存放文件描述符,并且时常会有插入,查找和删除的操作发生,这对内核的效率会产生不小的影响,因此需要一种插入,查找和删除效率都不错的数据结构来存放这些文件描述符,那么红黑树当然是不二的人选。

接下来我们来看看epoll底层是如何使用红黑树的

我们知道epoll在添加一个文件描述符进行监听或者删除一个文件描述符时使用的是epoll_ctl函数,该函数底层调用的是sys_epoll_ctl函数,下面给出该函数的部分源码

/*
 * The following function implements the controller interface for
 * the eventpoll file that enables the insertion/removal/change of
 * file descriptors inside the interest set.  It represents
 * the kernel part of the user space epoll_ctl(2).
 */
asmlinkage long
sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event)
{
	int error;
	struct file *file, *tfile;
	struct eventpoll *ep;
	struct epitem *epi;
	struct epoll_event epds;
 
	DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %p)\n",
		     current, epfd, op, fd, event));
 
	error = -EFAULT;
	if (EP_OP_HASH_EVENT(op) &&
	    copy_from_user(&epds, event, sizeof(struct epoll_event)))
		goto eexit_1;
 
	/* Get the "struct file *" for the eventpoll file */
	error = -EBADF;
	file = fget(epfd);
	if (!file)
		goto eexit_1;
 
	/* Get the "struct file *" for the target file */
	tfile = fget(fd);
	if (!tfile)
		goto eexit_2;
 
	/* The target file descriptor must support poll */
	error = -EPERM;
	if (!tfile->f_op || !tfile->f_op->poll)
		goto eexit_3;
 
	/*
	 * We have to check that the file structure underneath the file descriptor
	 * the user passed to us _is_ an eventpoll file. And also we do not permit
	 * adding an epoll file descriptor inside itself.
	 */
	error = -EINVAL;
	if (file == tfile || !IS_FILE_EPOLL(file))
		goto eexit_3;
 
	/*
	 * At this point it is safe to assume that the "private_data" contains
	 * our own data structure.
	 */
	ep = file->private_data;
 
	down_write(&ep->sem);
 
	/* Try to lookup the file inside our hash table */
	epi = ep_find(ep, tfile, fd);

在sys_epoll_ctl的参数中,op代表要进行的操作,fd表示要被操作的文件描述符。操作类型定义在下面着三个宏中

/* Valid opcodes to issue to sys_epoll_ctl() */
#define EPOLL_CTL_ADD 1
#define EPOLL_CTL_DEL 2
#define EPOLL_CTL_MOD 3

首先呢,会调用ep_find函数在内核事件表也就是红黑树中查找该fd是否已经存在,这里的结果会先保存在epi中,ep_find函数做了什么操作呢?这里就是我们第一个用到红黑树的地方:查找

【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)

先来看一下ep_find的实现:

/*
 * Search the file inside the eventpoll hash. It add usage count to
 * the returned item, so the caller must call ep_release_epitem()
 * after finished using the "struct epitem".
 */
static struct epitem *ep_find(struct eventpoll *ep, struct file *file, int fd)
{
	int kcmp;
	unsigned long flags;
	struct rb_node *rbp;
	struct epitem *epi, *epir = NULL;
	struct epoll_filefd ffd;
 
	EP_SET_FFD(&ffd, file, fd);
	read_lock_irqsave(&ep->lock, flags);
	for (rbp = ep->rbr.rb_node; rbp; ) {
		epi = rb_entry(rbp, struct epitem, rbn);
		kcmp = EP_CMP_FFD(&ffd, &epi->ffd);
		if (kcmp > 0)
			rbp = rbp->rb_right;
		else if (kcmp < 0)
			rbp = rbp->rb_left;
		else {
			ep_use_epitem(epi);
			epir = epi;
			break;
		}
	}
	read_unlock_irqrestore(&ep->lock, flags);
 
	DNPRINTK(3, (KERN_INFO "[%p] eventpoll: ep_find(%p) -> %p\n",
		     current, file, epir));
 
	return epir;
}

这里的for循环就是一个红黑树的查找过程,我们可以看到这里查找的时候用到的一个变量是kcmp,这个kcmp的值就是我们的fd在红黑树中所用来排序的值。而且我们可以看到这个kcmp的值来源于宏函数EP_CMP_FFD我们来看一下这个宏函数的实现

/* Compare rb-tree keys */
#define EP_CMP_FFD(p1, p2) ((p1)->file > (p2)->file ? +1: \
			    ((p1)->file < (p2)->file ? -1: (p1)->fd - (p2)->fd))

根据该宏函数的实现我们看到在比较时其实使用的是一个epoll_filefd的结构体中的file成员来比较的,那么我们再进入epoll_filefd中查看一下

我们看到这里的file是一个struct file类型的指针,当我们比较两个file类型的指针时比较的是他们的指针的值,也就是file结构体的地址。

根据源码判断,在红黑树中排序的根据是file的地址大小。至于为什么,目前还并不是很清楚,也存在我理解错误的可能,这里不是很确定。

查找完毕后,就要开始进行具体的操作了,这里会根据宏的值判断应该进行的操作是插入,删除,还是修改。这里给出sys_epoll_ctl的剩余源码(和文章开头给出的前半部分刚好衔接)

error = -EINVAL;
	switch (op) {
	case EPOLL_CTL_ADD:
		if (!epi) {
			epds.events |= POLLERR | POLLHUP;
 
			error = ep_insert(ep, &epds, tfile, fd);
		} else
			error = -EEXIST;
		break;
	case EPOLL_CTL_DEL:
		if (epi)
			error = ep_remove(ep, epi);
		else
			error = -ENOENT;
		break;
	case EPOLL_CTL_MOD:
		if (epi) {
			epds.events |= POLLERR | POLLHUP;
			error = ep_modify(ep, epi, &epds);
		} else
			error = -ENOENT;
		break;
	}
 
	/*
	 * The function ep_find() increments the usage count of the structure
	 * so, if this is not NULL, we need to release it.
	 */
	if (epi)
		ep_release_epitem(epi);
 
	up_write(&ep->sem);
 
eexit_3:
	fput(tfile);
eexit_2:
	fput(file);
eexit_1:
	DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_ctl(%d, %d, %d, %p) = %d\n",
		     current, epfd, op, fd, event, error));
 
	return error;
}

我们看到这部分代码里最主要的工作就是进行这个switch,case语句所做的判断工作了,这里sys_epoll_ctl函数根据参数op的不同而调用不同的函数进行处理,我们以EPOLL_CTL_ADD宏举例,该宏要进行的操作是插入一个新的文件描述符。

epoll底层的红黑树插入是调用ep_insert插入的,而ep_insert函数里面调用了ep_rbtree_insert来进行对红黑树中一个节点的插入。这两个函数的声明如下:

static void ep_rbtree_insert(struct eventpoll *ep, struct epitem *epi);
static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
		     struct file *tfile, int fd);

我们忽略ep_insert函数其他的实现要点,直接查看它所调用的函数ep_retree_insert的实现

static void ep_rbtree_insert(struct eventpoll *ep, struct epitem *epi)
{
	int kcmp;
	struct rb_node **p = &ep->rbr.rb_node, *parent = NULL;
	struct epitem *epic;
 
	while (*p) {
		parent = *p;
		epic = rb_entry(parent, struct epitem, rbn);
		kcmp = EP_CMP_FFD(&epi->ffd, &epic->ffd);
		if (kcmp > 0)
			p = &parent->rb_right;
		else
			p = &parent->rb_left;
	}
	rb_link_node(&epi->rbn, parent, p);
	rb_insert_color(&epi->rbn, &ep->rbr);
}

可以看到这里在插入一个新节点时对于其在红黑树中的位置的选择过程是用一个while循环来实现的,当该while循环退出后,说明我们已经找到了该节点应在的位置,接下来调用rb_link_node函数将该节点插入到红黑树中,该函数的实现很简单,就是往一颗二叉树中插入一个新的节点,实现如下

static inline void rb_link_node(struct rb_node * node, struct rb_node * parent,
				struct rb_node ** rb_link)
{
	node->rb_parent = parent;
	node->rb_color = RB_RED;
	node->rb_left = node->rb_right = NULL;
 
	*rb_link = node;
}

然后再调用rb_insert_color函数,这个函数实现的是对插入一个新节点之后的整个红黑树进行调整的过程,这里牵扯到红黑树的旋转,不是我们本文的重点,只把代码贴上,有兴趣的同学可以下去自习。

void rb_insert_color(struct rb_node *node, struct rb_root *root)
{
	struct rb_node *parent, *gparent;
 
	while ((parent = node->rb_parent) && parent->rb_color == RB_RED)
	{
		gparent = parent->rb_parent;
 
		if (parent == gparent->rb_left)
		{
			{
				register struct rb_node *uncle = gparent->rb_right;
				if (uncle && uncle->rb_color == RB_RED)
				{
					uncle->rb_color = RB_BLACK;
					parent->rb_color = RB_BLACK;
					gparent->rb_color = RB_RED;
					node = gparent;
					continue;
				}
			}
 
			if (parent->rb_right == node)
			{
				register struct rb_node *tmp;
				__rb_rotate_left(parent, root);
				tmp = parent;
				parent = node;
				node = tmp;
			}
 
			parent->rb_color = RB_BLACK;
			gparent->rb_color = RB_RED;
			__rb_rotate_right(gparent, root);
		} else {
			{
				register struct rb_node *uncle = gparent->rb_left;
				if (uncle && uncle->rb_color == RB_RED)
				{
					uncle->rb_color = RB_BLACK;
					parent->rb_color = RB_BLACK;
					gparent->rb_color = RB_RED;
					node = gparent;
					continue;
				}
			}
 
			if (parent->rb_left == node)
			{
				register struct rb_node *tmp;
				__rb_rotate_right(parent, root);
				tmp = parent;
				parent = node;
				node = tmp;
			}
 
			parent->rb_color = RB_BLACK;
			gparent->rb_color = RB_RED;
			__rb_rotate_left(gparent, root);
		}
	}
 
	root->rb_node->rb_color = RB_BLACK;
}

相关推荐

linux sed系列 第四篇:sed工业实战——日志处理与数据清洗

“掌握了sed的编程能力后,我们如同装备精良的工匠,终于可以踏入真实的工业战场。本篇将聚焦sed在日志分析、数据合规化、多文件批处理等场景中的应用,看它如何在海量数据中游刃有余,展现文本处理的...

Linux下sed的简单使用(linux中sed是什么意思)

1、sed简介stremeditor流编辑器,它是一项Linux指令,功能同awk类似,差别在于,sed简单,对列处理的功能要差一些,awk的功能复杂,对列处理的功能比较强大,sed编辑器是一行一...

linux基础命令之date命令(linux中的date)

date命令主要用于显示或者设置系统时间语法格式:date参数对象使用date命令时,最好先使用date--help命令查看支持哪些参数,有些小型Linux系统下的date命令,只支持一些基本参...

Ubuntu linux 常用命令(ubuntu常用的50个命令)

使用dpkg命令来安装.deb包。sudodpkg-i~/example.deb如果在安装过程中遇到依赖问题,可以使用以下命令来修复:sudoapt-getinstall-f将flut...

Linux基础命令-sed命令(linux教程:sed命令的用法)

Sed全名streameditor流编辑器,它是一个强大的文本处理工具,它可以从文件中接受输入,也可以接受来自标准输入流的输入,它擅长取行。Sed的用途非常广泛,包括:1)文本替换2)选择性的输...

linux sed系列 第二篇:sed进阶技巧——地址定位与正则表达式

“上一篇我们掌握了sed的基础替换,如同获得了第一把钥匙。现在,让我们更进一步,学习如何精准锁定目标行,如同拥有了导航地图,让每一次操作都直击要害!”地址定位的四种维度sed的强大,很大程度上源...

火狐Firefox浏览器140发布:手动Unload标签页、优化翻译体验等

IT之家6月24日消息,Mozilla在发布版本139不到一个月后,推出了最新的开源网页浏览器Firefox140。新版本增加了手动Unload标签页的功能,优化了垂直标签页的调...

Linux 基本正则表达式及扩展正则表达式功能举例

在Linux中,正则表达式(RegularExpression)是一种强大的模式匹配工具,用于在文本中查找、匹配和处理特定模式的字符串。Linux支持两种类型的正则表达式:基本正则表达式(Basic...

linux下find命令的经典26个使用示例

简介find命令是基于unix的操作系统中常用的工具之一。顾名思义,它在目录层次结构中查找文件和目录。用户可以传递不同的参数,并根据文件的名称、扩展名、类型、大小、权限、修改时间、所有者、组等搜索文件...

linux运维中特殊符号的应用与实践

路径位置类的特殊符号(1)、波浪线(~)在linux系统的命令行中,~表示用户的家目录,超级用户为/root,普通用户为/home。假设我当前目录在usr/local下[root@xrylocal]...

开源框架log4cpp实战(开源gui框架)

1.Log4cpp使用Log4cpp中主要包含Category(种类),Appender(附加器),Layout(布局),Priorty(优先级),NDC(嵌套的诊断上下文)。Category、App...

Linux find命令详解(linux find -l)

一、命令介绍Linuxfind命令是类unix操作系统中最重要和最常用的命令行实用程序之一。find命令用于根据指定的条件搜索和定位与参数匹配的文件和目录列表。find命令提供了广泛的选项,允许用户...

Linux运维:单引号与双引号的使用(linux 单引号和双引号)

1、单引号的使用单引号可以将它中间的所有任意字符还原为字面意义,实现屏蔽Shell元字符的功能。注意不可以在两个单引号中间单独插入一个单引号,单引号必须成对出现。示例1:定义一个变量,并输出变量的...

Linux技巧:find 命令用法详细说明,看完会有收获

在Linux命令中,find是比较复杂难用的命令。使用该命令搜索文件时,常常发现自己找了一些例子能用,但稍微改一下条件,就搜不到想要的结果。下面会以一些实例来说明使用find命令的关键要点和...

Linux Shell中单引号、双引号、反引号的解释

1、单引号('')单引号所见即所得,直接显示单引号里的内容。即单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的。比如下面的例子,单引号所见即所得。2、双引号("...