epoll的实现原理(1)

本文详细介绍了epoll在Linux内核中的实现原理,包括epoll实例的创建、如何保存监控的文件描述符,以及ep_find()和ep_insert()函数的作用。epoll利用红黑树结构跟踪监控的文件描述符,并通过epoll的回调函数注册到文件描述符中,实现事件发生时的通知。
摘要由CSDN通过智能技术生成

epoll的实现原理(1)

本文是学习epoll过程中的笔记,方便自己理解,基本翻译自下面的文章:

The Implementation of epoll(1)

The Implementation of epoll(2)

概述

epoll与传统的I/O多路复用技术之间的最大差别在于,用户只需要获取一个epoll实例,然后将文件描述注册到它上面(一次性),而不是每次将大量文件描述符传递到内核当中。

epoll实例的创建

通过调用epoll_create(2)或者epoll_create1(2)来请求实例,返回的是文件描述符。所以,epoll也是可以轮询的,用于一些高级用法。

epoll实例实际的重要部分是内核数据struct eventpoll,这个数据结构几乎维护了epoll实例正常工作所需要的所有内容。它的创建方法如下:

/*
 * Create the internal data structure ("struct eventpoll").
 */
error = ep_alloc(&ep);

ep_alloc()所做的,只是从内核堆分配足够的空间来保存eventpoll (ep),然后epoll_create()去尝试在进程中获取未使用的描述符。如果能够获取到描述符,它会尝试从系统获取一个匿名的inode (什么是匿名inode?)。 这里注意,epoll_create()eventpoll的指针保存在了文件的private_data 中,这样访问eventpoll非常方便。

然后,epoll_create()将匿名inode与文件描述符绑定起来,并向用户返回文件描述符fd。

epoll实例如何保存它监控的文件描述符

epoll使用非常常用的内核数据结构 - 红黑树(缩写为RB-Tree),以跟踪当前由特定epoll实例监视的所有文件描述符。RB-Tree的根是其struct eventpollrbr成员,在函数ep_alloc()内初始化。

对于epoll所监控的文件描述符,epoll使用一个与之关联的struct epitem 保存在红黑树当中。struct epitem 中同时还保存了一些其他的重要数据供epoll使用。

ep_find()

当我们使用epoll_ctl(2) 向epoll实例加入一个文件描述符的时候,首先要调用ep_find()去定位epitem在红黑树中的位置。红黑树的键值为存储在epitem中的struct epoll_filefd

struct epoll_filefd {
	struct file *file; // pointer to the target file struct corresponding to the fd
	int fd; // target file descriptor number
} __packed;

键值比较方式如下,struct file地址大就更大,相同就比较描述符号

/* 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));
}

当添加fd的时候,需要找到一个空位置,如果没找到合适的位置会返回错误EEXIST。找到后,开始调用ep_insert()

ep_insert()

目的是将epoll的回调函数注册到文件描述符,让fd在事件发生时通知epoll。

重点操作的对象:struct epitem(最后需要插入到epoll的RB-tree中的struct),监控的文件描述符

1.确保用户监控的fd总数不超,然后从slab分配struct epitem的内存
/* Item initialization follow here ... */
INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->pwqlist);    //保存struct eppoll_entry *pwq的链表,后面有讲
epi->ep = ep;                     //epoll实例的eventpoll
ep_set_ffd(&epi->ffd, tfile, fd); //设置fd
epi->event = *event; //监控的事件
epi->nwait = 0; //就是epi->pwqlist链表的长度,后面有奖
epi->next = EP_UNACTIVE_PTR;
2.尝试将epoll的回调函数注册到文件描述符

介绍一些需要的struct

struct ep_pqueue {         //包装epitem与poll_table
	poll_table pt;
    struct epitem *epi;
};

typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);//回调函数

typedef struct poll_table_struct { //poll_table
	poll_queue_proc _qproc;        //回调函数,待会让poll_wait执行!
	unsigned long _key;            //设置为~0,表示任何事件都要通知epoll,然后在epoll实现中过滤
} poll_table;

初始化struct ep_pqueue(设置epitem指针epi,设置poll_table的回调函数和事件)

3.epoll调用ep_item_poll(epi, &epq.pt),这将调用相应文件fd的poll()来实现。

epi:epitemepq.pt:poll_tabel(包含回调函数)

//例如TCP的poll函数
unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
	unsigned int mask;
	struct sock *sk = sock->sk;
	const struct tcp_sock *tp = tcp_sk(sk);

	sock_rps_record_flow(sk);
    
	sock_poll_wait(file, sk_sleep(sk), wait);//参数:文件,套接字,wait就是poll_tabel
    //sk_sleep(sk) 返回特定sock的等待队列!
	// code omitted
}

sock_poll_wait()只是进行一些检查,然后用相同的参数调用poll_wait()

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
	if (p && p->_qproc && wait_address)      //回调函数 && 等待队列 存在
		p->_qproc(filp, wait_address, p);   //_qproc函数保存poll_table中,定义见下
}
/*
* This is the callback that is used to add our wait queue to the
* target file wakeup lists.           _qproc函数
*/
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
			 poll_table *pt)
{
	struct epitem *epi = ep_item_from_epqueue(pt);  //恢复指向的epitem
	struct eppoll_entry *pwq;                       //创建一个epitem与fd的关联(glue)
													//后添加到epitem
	if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
        //用ep_poll_callback初始化pwq->wait
		init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
		pwq->whead = whead;       //保存等待队列头,方便epoll取消在队列中的注册
		pwq->base = epi;
        //把pwq->wait(ep_poll_callback)加入到(特定sock的)等待队列
		add_wait_queue(whead, &pwq->wait);
		list_add_tail(&pwq->llink, &epi->pwqlist);   //把eppoll_entry的加到epi->pwqlist链表
		epi->nwait++;                                //代表上面链表的长度,通常为1,不知道有啥用
	} else {
		/* We have to signal that an error occurred */
		epi->nwait = -1;
	}
}

pwq->wait(ep_poll_callback)的作用:

  1. 监视在该特定受监视文件上发生的事件
  2. 必要时唤醒其他进程

最后通过ep_rbtree_insert(ep, epi)struct epitem添加到红黑树中

4.疑点解释
  • 加入到wait_queue_head_t中的wait_queue_t(ep_poll_callback)通常是基于机器的唤醒机制,是一个包含函数指针struct。在函数中,epoll可以选择如何处理这个唤醒信号(不关心的事件就不处理)。
  • 对于上面提到的poll(),完全取决于它是怎么实现的。例如对于TCP套接字的fd来说,等待队列在它的成员strcut sock中,可以见得,不同的文件实现会将他的等待队列头放在完全不同的位置(是不确定的),所以我们只能将ep_ptable_queue_proc()交给poll()以回调的方式执行!
  • sk_wp内部的sock(等待队列)唤醒的时机?
//struct sock defined the following hooks in net/core/sock.c, line 2312
void sock_init_data(struct socket *sock, struct sock *sk)
{
	// code omitted...
	 sk->sk_data_ready  =   sock_def_readable;
	sk->sk_write_space =  sock_def_write_space;
    // code omitted...
}

sock_def_readable()sock_def_write_space()函数内, (struct sock)->sk_wq在执行唤醒回调时调用了 wake_up_interruptible_sync_poll()

对于tcp来说,sk->sk_data_ready()只要TCP连接完成三次握手,或者已经收到特定TCP套接字的缓冲区,就会在下面的中断处理程序调用。sk->sk_write_space()只要从该套接字发生从full-> available的缓冲区状态转换,就会调用它。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值