Epoll原理及实现

Epoll原理及实现

  • 为什么会有Epoll?
  • Epoll create
  • Epoll add
  • 数据结构间关系
  • 后记

为什么会有Epoll?

  • 讲Epoll可能就不得不对早期的poll和select函数进行比较了。他们同属于多路IO并发监听,个人理解就是可以同时监听多个文件描述符的状态,如果有数据到来就返回对应的文件描述符,这样避免了读取一个文件描述符被阻塞,如果轮询又造成CPU浪费的情况
    同属于多路并非监听,select函数在监听到IO事件后,只是设置了标志位,返回了整个监听描述符的数组,在这种情况下,如果需要知道是哪个文件描述符有数据了,则需要遍历整个数组查看标志位,读完数据后,需要重新设置数组,然后再进行轮询操作,这种情况在文件描述符过多的情况下会造成cpu的数组拷贝(将整个数组拷贝到用户态,然后又拷贝到内核态)和遍历数组的消耗(遍历数组找到设置标志位的文件描述符),遍历复杂的为o(n),明显随着文件描述符的数量增加而变复杂
    为了解决这一消耗,便出现了epoll这个函数,epoll在添加完监听描述符后,不需要再次添加,同时在有IO事件时,也只返回有IO变化的描述符,这样提高了效率和并发的性能。

Epoll create

  • 对于epoll的创建,调用系统提供的系统函数即可:
/**
* @param size 废弃,但是不能小于0
* *
@returns 返回一个epoll句柄(即一个文件描述符)
*/
int epoll_create(int size);

系统函数调用值得注意的是,参数size不再是指定监听的文件描述符个数,它已经被忽视了,但是为了保持兼容,不能小于0。当然我们更关心的是内核态是怎么做的,在Linux源码目录fs/eventpoll.c文件中定义了对应的系统调用:

SYSCALL_DEFINE1(epoll_create, int, size)
{
	if (size <= 0)
		return -EINVAL;

	return do_epoll_create(0);
}

可以看到函数传递的size并没有被最终传递下去,但是如小于0则会被返回错误。继续往下看函数调用:

/*****************************************************************************
 Prototype    : do_epoll_create
 Description  : 创建epoll 文件描述符
 Input        : int flags  
 Output       : None
 Return Value : static
 Calls        : 
 Called By    : 
 
  History        :
  1.Date         : 2019/5/3
    Author       : 
    Modification : Created function

*****************************************************************************/
static int do_epoll_create(int flags)
{
	int error, fd;
	struct eventpoll *ep = NULL;
	struct file *file;

	if (flags & ~EPOLL_CLOEXEC)
		return -EINVAL;
		
	 /* 分配struct eventpoll结构体并初始化 */
	error = ep_alloc(&ep);

	 /* 获取当前进程一个没有使用的文件描述符 */
	fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));

    /* fd绑定任意iNode,并创建返回 struct file结构体,并将ep指针赋值file的私有数据域 */
	file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,
				 O_RDWR | (flags & O_CLOEXEC));
				 
    /* 将file指针赋值给eventpoll */
	ep->file = file;
    /* 将fd和file关联 */
	fd_install(fd, file);
	
	return fd;

}

其实这个的文件描述符创建,我们关心的应该是eventpoll_fops函数操作集合,以及将创建的ep保存在file的私有数据中。同样该关心的结构体则是struct eventpoll *ep它有哪些数据成员,以及这些数据是怎样组织起来的?

Epoll add

  • 向epoll中添加文件监听的文件描述符,调用系统的epoll_ctl函数:
/**
* @param epfd 用epoll_create所创建的epoll句柄
* @param op 表示对epoll监控描述符控制的动作
* *
EPOLL_CTL_ADD(注册新的fd到epfd)
* EPOLL_CTL_MOD(修改已经注册的fd的监听事件)
* EPOLL_CTL_DEL(epfd删除一个fd)
* *
@param fd 需要监听的文件描述符
* @param event 告诉内核需要监听的事件
* *
@returns 成功返回0,失败返回-1, errno查看错误信息
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event
 {
	__uint32_t events; /* epoll 事件 */
	epoll_data_t data; /* 用户传递的数据 */
}

 /* *
	events : {EPOLLIN, EPOLLOUT, EPOLLPRI,
	EPOLLHUP, EPOLLET, EPOLLONESHOT}
*/
typedef union epoll_data 
{
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
} epoll_data_t;

同样我们也关心一下内核是怎么实现的:

/*****************************************************************************
 Prototype    : SYSCALL_DEFINE4
 Description  : 定义epoll_ctl函数,添加、删除监听描述符
 Input        : epfd epoll的描述符,fd 被监听的描述符                                         
 Output       : None
 Return Value : 
 Calls        : 
 Called By    : 
 
  History        :
  1.Date         : 2019/5/3
    Author       : 
    Modification : Created function

*****************************************************************************/
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
		struct epoll_event __user *, event)
{
	int error;
	int full_check = 0;
	struct fd f, tf;
	struct eventpoll *ep;
	struct epitem *epi;
	struct epoll_event epds;
	struct eventpoll *tep = NULL;

	error = -EFAULT;
    /* 判断是否有传递event参数 */
	if (ep_op_has_event(op) &&
	    copy_from_user(&epds, event, sizeof(struct epoll_event)))
		goto error_return;

	error = -EBADF;
    /* 获取struct fd结构体,存储有关文件的信息,fops等 */
	f = fdget(epfd);
	if (!f.file)
		goto error_return;

	/* Get the "struct file *" for the target file */
    /* 获取目的文件的struct fd结构体 */
	tf = fdget(fd);
	if (!tf.file)
		goto error_fput;

	/* The target file descriptor must support poll */
	error = -EPERM;
    /* 检查目的文件是否支持epoll */
	if (!file_can_poll(tf.file))
		goto error_tgt_fput;

	/* Check if EPOLLWAKEUP is allowed */
	if (ep_op_has_event(op))
		ep_take_care_of_epollwakeup(&epds);

	error = -EINVAL;
    /* 检查加入epoll的fd本身不能是epoll类型的 */
	if (f.file == tf.file || !is_file_epoll(f.file))
		goto error_tgt_fput;

	 /* 通过文件结构体的私有数据获取到struct eventpoll,ep是创建epoll fd的时候创建的 */
	ep = f.file->private_data;

	if (op == EPOLL_CTL_ADD) 
    {
		if (!list_empty(&f.file->f_ep_links) ||
					    is_file_epoll(tf.file)) 
        {
			if (is_file_epoll(tf.file)) 
           {
				if (ep_loop_check(ep, tf.file) != 0)
                {
					clear_tfile_check_list();
				}
                
			} 
            else
            {
                list_add(&tf.file->f_tfile_llink, &tfile_check_list);
            }
			
			if (is_file_epoll(tf.file)) 
            {
				tep = tf.file->private_data;
			}
		}
	}

	 /* 遍历rb tree 查找加入epoll的文件描述符对应的struct epitem */
	epi = ep_find(ep, tf.file, fd);

	error = -EINVAL;
	switch (op) 
   {
        
	case EPOLL_CTL_ADD:
		if (!epi) 
        {
            /* epi为空表面之前没有加入到红黑树中,则需要将节点加入到rb tree中,系统会默认为添加EPOLLERR  EPOLLHUP事件 */
			epds.events |= EPOLLERR | EPOLLHUP;
			error = ep_insert(ep, &epds, tf.file, fd, full_check);
		} 
        else
        {
    		if (full_check)
    			clear_tfile_check_list();
            
    		break;
        }
        
	case EPOLL_CTL_DEL:
        {
    		if (epi)
                /* 将epi移除rb tree */
    			error = ep_remove(ep, epi);
    		else
    			error = -ENOENT;
            
    		break;
        }  
    
	case EPOLL_CTL_MOD:
		if (epi) 
        {
			if (!(epi->event.events & EPOLLEXCLUSIVE)) 
            {
				epds.events |= EPOLLERR | EPOLLHUP;
				error = ep_modify(ep, epi, &epds);
			}
		} 
        else
			error = -ENOENT;
		break;
	}

	return error;
}

这个函数看起来稍微有点复杂,首先是进行了一堆参数错误检查,其实关注流程我们只需要看几个重要的函数:

1. epi = ep_find(ep, tf.file, fd);
2. error = ep_insert(ep, &epds, tf.file, fd, full_check);
3. error = ep_remove(ep, epi);
4. error = ep_modify(ep, epi, &epds);

首先函数ep_find是为了保证对应fd(被监听的文件描述符)的epi确实存在,否则都不做操作,这里其实我们就可以分析该函数,分析前大概想想,这个函数肯定要去遍历整个数据结构体,找到对应的项,然后返回。那么其实这个数据结构就是epi的数据存储方式。
那么什么是epi?epi对应的结构体代表什么?我们可以在ep_insert函数找到答案。我们首先看看ep_find函数:

static struct epitem *ep_find(struct eventpoll *ep, struct file *file, int fd)
{
	int kcmp;
	struct rb_node *rbp;
	struct epitem *epi, *epir = NULL;
	struct epoll_filefd ffd;

	ep_set_ffd(&ffd, file, fd);
	for (rbp = ep->rbr.rb_root.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 {
			epir = epi;
			break;
		}
	}

	return epir;
}

/* Compare RB tree keys */
static inline int ep_cmp_ffd(struct epoll_filefd *p1,
			     struct epoll_filefd *p2)
{
	return (p1->file > p2->file ? +1:
	        (p1->file < p2->file ? -1 : p1->fd - p2->fd));
}

rbp = ep->rbr.rb_root.rb_node其实就可以看出epi存的数据结构体是一颗红黑树,红黑树的root节点,在ep(创建epoll fd会创建一个ep)中保存
那么epi存的是什么?我们继续看函数ep_insert函数:

static int ep_insert(struct eventpoll *ep, const struct epoll_event *event,
		     struct file *tfile, int fd, int full_check)
{
        /* 从slab池中分配epi节点 */
	if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
		return -ENOMEM;

	/* 初始化epi */
	INIT_LIST_HEAD(&epi->rdllink);
	INIT_LIST_HEAD(&epi->fllink);
	INIT_LIST_HEAD(&epi->pwqlist);
	epi->ep = ep; /* 设置指向ep的指针 */
	
	/* 存储fd和file的值 */
	ep_set_ffd(&epi->ffd, tfile, fd);
	epi->event = *event;
	epi->nwait = 0;
	epi->next = EP_UNACTIVE_PTR;
	
	epq.epi = epi;
	
	 /* epi加入到rb tree中 */
	ep_rbtree_insert(ep, epi);

}

/* Setup the structure that is used as key for the RB tree */
static inline void ep_set_ffd(struct epoll_filefd *ffd,
			      struct file *file, int fd)
{
	ffd->file = file;
	ffd->fd = fd;
}

函数中删除了一些代码,便于更好的理解,我们可以理解为向epoll中加入一个fd,就会创建一个epi(struct epitem *epi;)并加入到ep中的rb tree保存

数据结构间关系

Epoll原理及实现_第1张图片
由代码分析可以得到如图这样一个数据关系,这个数据关系有利于对代码的理解,毕竟C语言操作的对象都是数据~

后记

这次写的博客时间较短,可能会存在一些疏漏和不严谨,博客有一部分内容是借鉴https://www.cnblogs.com/sduzh/p/6714281.html来的,写这篇文章的主要目的还是自己学习总结和记录,O(∩_∩)O谢谢

你可能感兴趣的:(Epoll原理及实现)