epoll原理总结:

说起epoll,不得不谈起它的三个函数,这3个函数是系统提供的,
一般来说,我们学会使用就行了,但是有时候,我们遇到了问题,
就必须要懂得这几个函数的原理了。

我们通过wangbojing网友的NtyTcp项目学习一下epoll的原理。

(1)int epoll_create(int size);
该函数创建了一个epoll对象,返回了一个文件描述符来标识这个对象,
所以,最后,我们要用close来关闭这个返回的文件描述符。
而关于它的参数,网上的说法很多,这里的话,不说太多了,先保证它大于0吧。

//创建epoll对象,创建一颗空红黑树,一个空双向链表
int epoll_create(int size) 
{
	if (size <= 0) return -1;

	nty_tcp_manager *tcp = nty_get_tcp_manager();
	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)相当于new了一个eventpoll对象【开辟了一块内存】
	struct eventpoll *ep = (struct eventpoll*)calloc(1, sizeof(struct eventpoll)); //参数1:元素数量 ,参数2:每个元素大小
	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;

	if (pthread_mutex_init(&ep->mtx, NULL)) {
		free(ep);
		nty_free_socket(epsocket->id, 0);
		return -2;
	}

	if (pthread_mutex_init(&ep->cdmtx, NULL)) {
		pthread_mutex_destroy(&ep->mtx);
		free(ep);
		nty_free_socket(epsocket->id, 0);
		return -2;
	}

	if (pthread_cond_init(&ep->cond, NULL)) {
		pthread_mutex_destroy(&ep->cdmtx);
		pthread_mutex_destroy(&ep->mtx);
		free(ep);
		nty_free_socket(epsocket->id, 0);
		return -2;
	}

	if (pthread_spin_init(&ep->lock, PTHREAD_PROCESS_SHARED)) {
		pthread_cond_destroy(&ep->cond);
		pthread_mutex_destroy(&ep->cdmtx);
		pthread_mutex_destroy(&ep->mtx);
		free(ep);

		nty_free_socket(epsocket->id, 0);
		return -2;
	}
	tcp->ep = (void*)ep;
	epsocket->ep = (void*)ep;
	return epsocket->id;
}

通过以上代码,我们重点关注注释当中的3件事情,
1:创建了一个eventpoll对象,这个对象可以想成是系统定义的结构。

//调用epoll_create()的时候我们会创建这个结构的对象
struct eventpoll {
	ep_rb_tree rbr;      //ep_rb_tree是个结构,所以rbr是结构变量,这里代表红黑树的根;
	int rbcnt;
	LIST_HEAD( ,epitem) rdlist;    //rdlist是结构变量,这里代表双向链表的根;
	/*	这个LIST_HEAD等价于下边这个 
		struct {
			struct epitem *lh_first;
		}rdlist;
	*/
	int rdnum; //双向链表里边的节点数量(也就是有多少个TCP连接来事件了)
	int waiting;
    /*定义一个互斥量,更新红黑树时使用*/
	pthread_mutex_t mtx; //rbtree update
	/*定义一个自旋锁,更新双向链表时使用*/
	pthread_spinlock_t lock; //rdlist update
	/*条件变量与互斥量配套使用*/
	pthread_cond_t cond; //block for event
	pthread_mutex_t cdmtx; //mutex for cond
};

2:红黑树根节点初始化,这时这颗红黑树当中还没有任何的节点。
3:双向链表初始化,这时双向链表当中还没有任何的节点。

现在整体看起来是这样子的一个结构:
在这里插入图片描述
(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event结构的定义如下所示:

struct epoll_event 
{
  uint32_t     events;      /* Epoll 事件 */
  epoll_data_t data;        /* 用户数据 */
};
typedef union epoll_data 
{
  void        *ptr;
  int          fd;
  uint32_t     u32;
  uint64_t     u64;
} epoll_data_t;

函数功能描述:把一个socket及socket相关的事件添加到epoll对象描述符上来,通过这个epoll对象来监视socket上的数据来往情况。

参数1,int epfd:为epoll_create函数的返回值。
参数2, int op,理解成一个数字就行,系统把这个选项分成了EPOLL_CTL_ADD,EPOLL_CTL_DEL,EPOLL_CTL_MOD。这个后面看源码再说。
参数3:相关的套接字/文件描述符
参数4:这个参数其实就是相当于红黑树节点的一个成员。

来看该函数的实现:

//往红黑树中加每个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];
	//struct _nty_socket *socket = tcp->fdtable->sockfds[sockid];

	if (epsocket->socktype == NTY_TCP_SOCK_UNUSED) {
		errno = -EBADF;
		return -1;
	}
	if (epsocket->socktype != NTY_TCP_SOCK_EPOLL) {
		errno = -EINVAL;
		return -1;
	}
	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对象,大家注意这个结构epitem,这个结构对象,其实就是红黑的一个节点,也就是说,红黑树的每个节点都是 一个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连接才能走到这里】;

		//(1)从红黑树上把这个节点干掉
		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) {
			//(1)红黑树上有该节点,则修改对应的事件
			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;
}

我们先关心op为EPOLL_CTL_ADD的情况,其它的可以举一反三。

注意改变量:

//这是个节点相关的结构
//作为红黑树的一个节点
struct epitem {
	RB_ENTRY(epitem) rbn;
	/*  RB_ENTRY相当如定义了如下的一个结构成员变量
	struct {											
	struct type *rbe_left;		//指向左子树
	struct type *rbe_right;		//指向右子树
	struct type *rbe_parent;	//指向父节点
	int rbe_color;			    //该红黑树节点颜色
	} rbn*/

	LIST_ENTRY(epitem) rdlink;
	/*
	struct {									
		struct type *le_next;	//指向下个元素
		struct type **le_prev;	//前一个元素的地址
	}*/

	int rdy; //exist in list 是否这个节点是同时在双向链表中【这个节点刚开始是在红黑树中】	
	int sockfd;
	struct epoll_event event; 
};

例如:调用了一下函数时,epoll对象发生的一些变化。

int serv_sock;
struct epoll_event event;
event.events = EPOLLIN;  //读取数据事件
    event.data.fd = serv_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

现在的结构应该是这样的。
在这里插入图片描述
上面的epi代表一个红黑树的节点,成员rbn现在没有左右子树,所以指向NULL,而sockfd则代表红黑树的key,也就是epoll_ctl函数的第三个参数,最后一个成员
event则是epoll_ctl函数的第四个参数。

(3)最后一个函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);当某些事件发生了的时候,系统就会把这个红黑树当中的节点增加到一个双向链表当中去(调用epoll_create时创建的)。
比如一个三路握手来了,现在的结构应该是这样的,

在这里插入图片描述
所以说,epi其实是即作为红黑树的节点又作为双向链表的节点,rdlink现在的下一个节点应该是空的。

最后,其实就是把event复制给了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;

	//nty_socket_map *epsocket = &tcp->smap[epid];
	struct _nty_socket *epsocket = tcp->fdtable->sockfds[epid];
	if (epsocket == NULL) return -1;

	if (epsocket->socktype == NTY_TCP_SOCK_UNUSED) {
		errno = -EBADF;
		return -1;
	}

	if (epsocket->socktype != NTY_TCP_SOCK_EPOLL) {
		errno = -EINVAL;
		return -1;
	}

	struct eventpoll *ep = (struct eventpoll*)epsocket->ep;
	if (!ep || !events || maxevents <= 0) {
		errno = -EINVAL;
		return -1;
	}

	if (pthread_mutex_lock(&ep->cdmtx)) {
		if (errno == EDEADLK) {
			nty_trace_epoll("epoll lock blocked\n");
		}
		assert(0);
	}

	//(1)这个while用来等待一定的时间【在这段时间内,发生事件的TCP连接,相关的节点,会被操作系统扔到双向链表去【当然这个节点同时也在红黑树中呢】】
	while (ep->rdnum == 0 && timeout != 0) {

		ep->waiting = 1;
		if (timeout > 0) {

			struct timespec deadline;

			clock_gettime(CLOCK_REALTIME, &deadline);
			if (timeout >= 1000) {
				int sec;
				sec = timeout / 1000;
				deadline.tv_sec += sec;
				timeout -= sec * 1000;
			}

			deadline.tv_nsec += timeout * 1000000;

			if (deadline.tv_nsec >= 1000000000) {
				deadline.tv_sec++;
				deadline.tv_nsec -= 1000000000;
			}

			int ret = pthread_cond_timedwait(&ep->cond, &ep->cdmtx, &deadline);
			if (ret && ret != ETIMEDOUT) {
				nty_trace_epoll("pthread_cond_timewait\n");
				
				pthread_mutex_unlock(&ep->cdmtx);
				
				return -1;
			}
			timeout = 0;
		} else if (timeout < 0) {

			int ret = pthread_cond_wait(&ep->cond, &ep->cdmtx);
			if (ret) {
				nty_trace_epoll("pthread_cond_wait\n");
				pthread_mutex_unlock(&ep->cdmtx);

				return -1;
			}
		}
		ep->waiting = 0; 

	}

	pthread_mutex_unlock(&ep->cdmtx);

	//等一小段时间,等时间到达后,流程来到这里。。。。。。。。。。。。。。

	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; 
}

那么是谁向双向链表当中增加节点呢?是操作系统干的,当发生某件事情的时候,系统会调用一个回调函数往双向链表当中增加节点。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值