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 eventpoll
的rbr
成员,在函数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:epitem
, epq.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)的作用:
- 监视在该特定受监视文件上发生的事件
- 必要时唤醒其他进程
最后通过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的缓冲区状态转换,就会调用它。