epoll原理学习笔记

目录

一、epoll的数据结构

1.epoll数据结构的选择

2.epoll数据结构的分析

二、epoll接口函数

三、epoll锁机制

1.list加锁操作

2.rbtree的加锁操作

3.epoll_wait的加锁操作

四、epoll回调函数

五、水平触发和边沿触发


epoll是Linux下IO多路复用接口select/poll的增强版本,是linux平台高性能网络IO的必要组件。其有两种实现方式:内核态实现和用户态实现。内核态实现参考代码为fs/eventpoll.c,用户态参考实现代码为https://github.com/wangbojing/NtyTcp/blob/master/src/nty_e poll_rb.c。虽然其实现方式不一样,实现细节有差异,但是原理是一样的。

为什么epoll有两种实现方式呢?因为协议栈有用户态和内核态这两种实现方式,而epoll是基于协议栈实现的,所以epoll也有两种实现方式。

本文主要参考用户态的epoll实现代码来学习epoll原理。epoll的原理可以从五个方面来理解:

1.epoll的数据结构。rbtree用于存储<fd,event>对, ready list用于存储就绪IO。

2.epoll接口函数。用于供应用程序使用epoll功能。

3.epoll的线程安全。SMP的运行,以及防止死锁。

4.epoll协议栈回调。

5.epoll的LT(水平触发)和ET(边沿触发)。

一、epoll的数据结构

1.epoll数据结构的选择

epoll需要处理大量fd。无论是epoll回调,还是epoll_ctl的ADD、DEL和MOD操作,都有查找操作。所以我们需要选择查找频率很高的数据结构。基于此,可供选择的数据结构有:

(1)哈希表:由于fd的数量是不确定的,而哈希表的大小是确定的。  使用哈希表可能会出现哈希表太大或太小的问题。      

(2)B/B+树:查找效率没有红黑树高,其层高适合做磁盘索引。

(3)红黑树:查找性能稳定,合适存储<fd,event>。所以,无论是epoll的内核实现还是用户态实现,都使用红黑树。

2.epoll数据结构的分析

epoll主要有两个结构体 :eventpoll 与 epitem 。

一个eventpoll对象代表一个epoll实例,其由epoll_create创建。

一个epitem对象代表一对<fd,event>,也是红黑树的一个节点。

结构体定义代码如下:

#define RB_ENTRY(type)										\
struct {													\
	struct type *rbe_left;		/* left element */		\
	struct type *rbe_right;		/* right element */		\
	struct type *rbe_parent;	/* parent element */		\
	int rbe_color;			/* node color */				\
}

#define	LIST_ENTRY(type)								\
	struct {											\
		struct type *le_next;	/* next element */	\
		struct type **le_prev;	/* address of previous next element */	\
	}


struct epitem {
	RB_ENTRY(epitem) rbn;
	LIST_ENTRY(epitem) rdlink;
	int rdy; //exist in ready list 
	
	int sockfd;
	struct epoll_event event; 
};

struct eventpoll {
	ep_rb_tree rbr;//red black tree root node
	int rbcnt;// red black node count
	
	LIST_HEAD( ,epitem) rdlist;//ready list node head
	int rdnum;// ready list node number

	int waiting;// When rdnum is zero and timeout is non-zero,waiting is 1, otherwise is 0

	pthread_mutex_t mtx; //Being used when update rbtree 
	pthread_spinlock_t lock; //Being used when update rdlist 
	
	pthread_cond_t cond; //block for event
	pthread_mutex_t cdmtx; //mutex for cond
	
};

数据结构如下图所示:

list 用来存储准备就绪的IO。当内核 IO 准备就绪的时候,则会执行 epoll_event_callback 的回调函数,将rbtree中 epitem对象 添加到 list 中,也就是说list和rbtree共用epitem对象。当 epoll_wait 激活重新运行的时候 ,将 list 的 epitem对象从list逐一 删除并将该对象的event值拷贝到 events 参数中 ,但是此epitem对象仍然还在rbtree中。

rbtree 用来存储所有 IO的数据,对此红黑树的操作主要通过epoll_ctl函数进行。

二、epoll接口函数

1.int epoll_create(int size);

参数size在此处是个无效的值,只要其大于0即可。

epoll_create主要用来创建eventpoll 实例并对其进行初始化,同时返回一个指向epoll实例的文件描述符。

2.int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl主要是对epitem对象进行操作:

  • EPOLL_CTL_ADD:先查看含fd的epitem对象是否在红黑树中,如果在则退出,不在则将fd和      event加到epitem对象中并将该对象插入红黑树中。
  • EPOLL_CTL_DEL:先查看含fd的epitem对象是否在红黑树中,如果不在则退出,在则将该epitem对象从红黑树中删除。
  • EPOLL_CTL_MOD:先查看含fd的epitem对象是否在红黑树中,如果不在则退出,在则更新事件。

3.int epoll_wait(int epfd, struct epoll_event *events,  int maxevents, int timeout);

epoll_wait函数主要是把就绪链表中的事件信息拷贝到events数组中,并返回就绪事件个数。

三、epoll锁机制

epoll从以下几个方面是需要加锁保护的 。List 的操作 ,rbtree 的操作 ,epoll_wait 的等待。
List使用最小粒度的锁 spinlock,便于在SMP下添加操作的时候, 能够快速操作list 。

1.list的加锁操作

避免SMP 体系下多核竞争 。 此处采用自旋锁,不适合采用睡眠锁。

a.list添加结点时加锁代码:

	pthread_spin_lock(&ep->lock);
	epi->rdy = 1;
	LIST_INSERT_HEAD(&ep->rdlist, epi, rdlink);
	ep->rdnum ++;
	pthread_spin_unlock(&ep->lock);

b.list删除结点时加锁代码:

	pthread_spin_lock(&ep->lock);

	int cnt = 0;
	int num = (ep->rdnum > maxevents ? maxevents : ep->rdnum);
	int i = 0;
	
	while (num != 0 && !LIST_EMPTY(&ep->rdlist)) { //EPOLLET

		struct epitem *epi = LIST_FIRST(&ep->rdlist);
		LIST_REMOVE(epi, rdlink);
		epi->rdy = 0;

		memcpy(&events[i++], &epi->event, sizeof(struct epoll_event));
		
		num --;
		cnt ++;
		ep->rdnum --;
	}
	
	pthread_spin_unlock(&ep->lock);

2.rbtree的加锁操作

a.rbtree添加结点时加锁操作代码:

pthread_mutex_lock(&ep->mtx);

		struct epitem tmp;
		tmp.sockfd = sockid;
		struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
		if (epi) {
			nty_trace_epoll("rbtree is exist\n");
			pthread_mutex_unlock(&ep->mtx);
			return -1;
		}

		epi = (struct epitem*)calloc(1, sizeof(struct epitem));
		if (!epi) {
			pthread_mutex_unlock(&ep->mtx);
			errno = -ENOMEM;
			return -1;
		}
		
		epi->sockfd = sockid;
		memcpy(&epi->event, event, sizeof(struct epoll_event));

		epi = RB_INSERT(_epoll_rb_socket, &ep->rbr, epi);
		assert(epi == NULL);
		ep->rbcnt ++;
		
		pthread_mutex_unlock(&ep->mtx);

b.rbtree删除结点时加锁代码

        pthread_mutex_lock(&ep->mtx);

		struct epitem tmp;
		tmp.sockfd = sockid;
		struct epitem *epi = RB_FIND(_epoll_rb_socket, &ep->rbr, &tmp);
		if (!epi) {
			nty_trace_epoll("rbtree no exist\n");
			pthread_mutex_unlock(&ep->mtx);
			return -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);

c.rbtree修改结点时,用户态代码没有加锁,但是内核态代码是加锁了的。个人认为加锁更合理。

3.epoll_wait的加锁操作

epoll_wait加锁代码如下:

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

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

四、epoll回调函数

在tcp的协议栈中,有四个地方会触发epoll的回调函数:

  1. 完成tcp 三次握手后 , 需要将监听 socket 的event 置为 EPOLLIN ,此时标识可以进入到 accept 读取 socket 数据 。
  2. 在established 状态,收到数据以后需要将socket 的 event 置为 EPOLLIN 状态 。
  3. 在 established 状态,收到 fin 时 ,socket 进入到 close _wait 。此时需要 socket 的event置为 EPOLLIN ,读取断开信息。
  4. 检测 socket 的 send 状态, 如果对端 cwnd>0 是可以发送数据的。故需要将 socket置为 EPOLLOUT 。

在此四处添加epoll 的回调函数,即可使得 epoll 正常接收到 IO 事件。

五、水平触发和边沿触发

水平触发:此时只要event设置了EPOLLIN,就能不断的触发epoll回调函数。

边沿触发:与之前的event相比,如果event 发生改变就会触发epoll回调函数 。 在此情形下变化只发生一次,故只调用一次 epoll 回调函数 。

本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux系统提升感兴趣的读者,可以点击链接,详细查看详细的服务:

服务器高级架构体系:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值