源码 【零声教育】四个-腾讯课


前言

Linux内核为用户操作epoll提供了3个关键函数,分别是:

操作系统使用一个回调函数()来调度epoll对象中的事件。这个函数非常重要,所以本文将对以上四个函数的源码进行分析。

源代码来源

由于epoll的实现是内嵌在内核中的,如果直接查看内核源码,一些不相关的代码会影响阅读。为此,上面写的简化版TCP/IP协议栈实现了epoll逻辑。链接是:

存放以上四个关键函数的文件是[src.c]。接下来本文通过分析程序的代码来探寻epoll能够支持高并发连接的秘密。

两大核心数据结构(一)

源码 【零声教育】四个-腾讯课

如图所示,它包含两个主要的成员变量,分别是rbn和rbn。前者是红黑树的结点,后者是双链表的结点,也就是说一个对象可以作为红黑树的结点。节点也可以用作双链表中的节点。而他们每一个都存储了一个事件,对事件的查询转化为对事件的查询。

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)

源码 【零声教育】四个-腾讯课

如图,包含两个主要的成员变量,分别是rbr和,前者指向红黑树的根节点,后者指向双链表的头节点。即一个对象对应两个容器。对的检索将发生在这两个容器(红黑树和双向链表)上。

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连接来事件了)
 
	// ...略...
	
};

四大关键功能(一)()

//创建epoll对象,包含一颗空红黑树和一个空双向链表
int epoll_create(int size) {
	//与很多内核版本一样,size参数没有作用,只要保证大于0即可
	if (size 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个步骤:

创建一个对象 让里面的rbr指向空 让对象里的点指向空 互斥 在并发环境下保存对象 返回对象的句柄(id)

相关视频推荐

epoll实战揭示——支撑亿级IO的底层基石

全网最详尽的epoll讲解,6种epoll设计,让你秒杀面试官

学习地址:C/C++ Linux服务器开发/后台架构师【零音教育】-学习视频教程-腾讯课堂

需要C/C++ Linux服务器架构师学习资料加采集(资料包括C/C++、Linux、技术、Nginx、、MySQL、Redis、、、ZK、流媒体、CDN、P2P、K8S、、TCP/IP、协程、DPDK等)源码,免费分享

源码 【零声教育】四个-腾讯课

(2)()

这个函数的逻辑其实很简单。无非就是将用户传入的参数封装成一个对象,然后根据传入的op是①、②还是③,决定是否①将该对象插入红黑树,②更新红黑树中的对象,或者③移除红黑树中的对象。

//往红黑树中加每个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)()

//到双向链表中去取相关的事件通知
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; 
}

这个函数的逻辑也很简单,就是我们先检查对象的双链表中是否有结点。如果有节点,则取出节点中的事件,填充到用户传入的指针指向的内存中。如果没有节点,则在while循环中等待一定时间,直到有事件被触发,操作系统才会将其插入到双向链表中,使得rdnum>0(这个过程由操作系统调用函数完成),程序会跳出while循环,从双向链表中取数据。

(4)()

通过跟踪它在内核中被调用的位置。可以看出当服务端会在以下5种情况下调用:

() in,服务端状态下三次握手完成,服务端状态下 close()断开连接,服务端状态下 send/write()数据状态,当服务器可读数据时,服务器可以发送

接下来我们看一下源码

//当发生客户端三路握手连入、可读、可写、客户端断开等情况时,操作系统会调用这个函数,用以往双向链表中增加一个节点【该节点同时 也在红黑树中】
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;
}

上面代码的逻辑也很简单,就是将红黑树的指向节点插入到双向链表中。

总结

epoll的底层实现有两个关键的数据结构,一个是另一个,里面有两个成员变量,分别是rbr和rbr。前者指向红黑树的根,后者指向双向链表的头部。它是红黑树节点和双向链表节点的复合体,也就是说,它既可以作为树的节点,也可以作为链表的节点源码,包含用户注册的事件。


© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容