在进行Epoll相关说明前,先大致介绍一下其操作基本单元socket。
一共用户进程定义:
struc task_struct{
pif_t pid;
struct file_struct *files;
}
也就是一个用户进程包含多个类型文件类,每个文件类定义是file_struct。
其中file_struct定义如下
struct file_struct{
struct fdtable*fdt;
}
struct fdtable{
strucy file **fd;
}
fd是一个文件链表的指针,每一个都是file类型,也就是每个文件类下面包含一个个文件列表,文件列表里每个元素都是一个file类型,其定义如下
struct file {
struc path f_path;
struct file_opeartions *f_op;
void *private_data;
}
其中,f_op包括aio_read(具体调用sock_aio_read),aio_write(具体调用sock_aio_write)和poll(具体调用socket对象的sock_poll);private_data指向有多种,最常见的是指向一个socket对象,其次在epoll场景中指向的是一个eventpoll对象。完整socket对象定义如下:
struct socket{
struct file*file;
struct sock*sk;
struc proto_ops*ops;
}
其中,ops包括accept(具体调用inet_accep)/poll(具体调用tcp_poll)/sendmsg(具体调用inet_sendmsg)/recvmsg(具体调用inet_recvmsg)等;sock类型完整定义:
struct sock{
sk_receive_queue;//接收队列,在数据到达时放入尾部
struct socket_q *sk_wq;//等待队列,在epoll_ctl_add添加socket时添加一个等待队列项(队列项包括flags/private_date/func)其中func为注册的回调函数ep_call_callback,最后放入队列头部;
void(*sk_data_ready);//函数指针,设置为sock_def_readable,在数据到达放入接收队列后ready调用,也是收据接收完成的调用的第一站!对于socket对象,sk_data_ready 将直接唤醒在 socket上等待的用户进程;对于eventpoll对象,此函数会找到在epoll_ctl_add添加socket时注册的回调函数ep_call_callback并调用;
}。
这里struct socket_q定义:
struct socket_q{
wait_queue_head_t wait;
}
等待队列相关的主要有2个,一个是等待队列头wait_queue_head_t,一个是等待队列项wait_queue_t,他们定义如下:
struct wait_queue_head_t{
lock;//在对task_list访问过程中加锁实现对等待队列的互斥访问
struct list_head task_list;//双向循环链表,每一个就是一个等待队列项
}
struct wait_queue_t{
int flags;//指明该等待过程是否互斥,0非互斥,1互斥
void *private;//指向阻塞的进程。在epoll_ctl_add中该等待队列项为nil,在epoll_wait中为阻塞的用户进程
wait_queue_func_t func;//注册的回调函数,在epoll_ctl_add注册的是ep_call_callback,在epoll_wait注册的是default_wake_function
void *base;//在epoll场景中指向该socket对应的epitem
}
可见等待队列头和等待队列项他们关系是等待队列是等待队列头的成员。也就是说等待队列头的task_list域链接的成员就是等待队列类型的(wait_queue_t)。
此外可以知道一个socket对象主要包括sock和ops。sock对象包含接收数据的接收队列,数据接收后的触发函数sk_data_ready以及在epoll_ctl_add添加socket时等待队列,等待队列里包括回调函数ep_call_callback。
说完socket,再介绍epoll。
epoll使用中有三个重要的函数:epoll_create(), epoll_ctl(), epoll_wait;
epoll_create:创建一个 eventpoll 对象,包括就绪队列/等待队列/红黑树等初始化;
epoll_ctl:针对一个socket对象,向eventpoll对象添加要管理的socket链接,具体包括
1.分配红黑树节点epitem,该节点主要成员ffd由socket对象的fd完成初始化,该节点主要成员*ep由eventpoll对象初始化;
2添加等待项到socket->sock的等待队列并注册回调函数ep_poll_callback;
2.1 获取该socket中sock等待队列头wait_queue_head_t,后续新建的等待队列项就能插入;
2.2 新建一个等待队列项wait_queue_t(flags=0,private=nil,func=ep_poll_callback,base=&epitem);
2.3 将等待队列项插入soccet中sock的等待队列*sk_wq;
3.将epitem插入红黑树;
epoll_wait:处理IO事件,阻塞。若eventpoll对象的就绪队列rdllist有就绪数据(epitem类)直接返回该数据;若无就绪事件,构造等待事件并关联和阻塞当前进程,放入eventpoll的等待事件队列wq并进入睡眠,待数据到达后唤醒。
3.1 eventpoll对象的就绪队列rdllist有就绪数据(epitem类),就直接返回该数据;
3.2 eventpoll对象的就绪队列rdllist无就绪数据,新建一个等待队列项wait_queue_t(flags=0,private=当前用户进程,func=default_wake_function,base=nil);
3.3 将等待队列项入eventpoll的等待队列wq;
3.4 阻塞进程并让出CPU;
epoll有两个重要的数据结构:struct eventpoll, struct epitem;
struct eventpoll
{
rwlock_t lock;
wait_queue_head_t wq;//等待时间队列,epoll_wait在无就绪事件,构造等待事件并关联和阻塞当前进程,放入eventpoll的等待事件队列wq并进入睡眠。
struct list_head rdllist;//就绪epitem队列,epoll_wait在有就绪事件后通过socket->sock->sd_data_ready调用注册在socket的sock的等待队列的某一个等待队列项的回调函数ep_poll_callback,ep_poll_callback函数根据等待队列项的base找到有就绪的epitem,并根据epitem的*ep找到eventpoll对象,并将epitem放入eventpoll对象rdllist;
struct rb_root rbr;//红黑树,存储epitem;
};
struct epitem
{
//红黑树的根节点,它的结点都为epitem变量。方便查找和删除
struct rb_node rbn;
//链表中的每个结点即为epitem中的rdllink,当epitem所对应的fd存在已经就绪的I/O事件,ep_poll_callback回调函数会将该结点连接到eventpoll对象中的rdllist循环链表中去,这样就将就绪的epitem都串起来了。
struct list_head rdllink;
//将fd和file绑定起来,由要插入eventpoll对象的socket对应的file和fd初始化
struct epoll_filefd{
struct file;
int fd;
} ffd;
//指向包含此epitem的所有poll wait queue,insert时,pwqlist=eppoll_entry->llink;
struct list_head pwqlist;
//eventpoll的指针,每个epitem都有这样一个指针,它指向对应的eventpoll变量。只要拿到了epitem,就可以根据它找到eventpoll
struct eventpoll *ep;
}
具体流程:
一 epoll_create,创建一个 eventpoll 对象ep
ep_alloc(&ep);-
1. 申请 epollevent 内存;
2.初始化等待队列头ep->wq;
3.初始化就绪列表ep->rdllist;
4.初始化红黑树指针rp->rbr;
二epoll_ctl,向eventpoll对象添加要管理的socket链接
当与客户端连接socket创建好,而且完成了ep对象的创建后,使用epoll_ctl注册每一个socket,主要完成3件事情:
1.分配一个红黑树节点epitem;
2.添加一个等待项到socker->sock->sk_wq等待队列中,并注册回调函数ep_call_callback;
3.将epitem插入到ep对象的红黑树中。
ep_insert(ep, &epds, tfile, fd);//其中edps是从用户拷贝过来的事件,tfile为待插入socket对应的file,fd是文件id;ep_inser主要做3件事情:
第一件事:
创建一个epitem对象epi并进行初始化,包括设置句柄等;
第二件事:设置socket->sk->sk_wq等待队列,并注册回调函数。具体实现:
ep_item_poll(epi, &epq.pt);-->epi->ffd.file->f_op->poll(epi->ffd.file, pt) & epi->event.events;-->sock_poll-->sock->ops->poll(file, sock, wait);-->tcp_poll-->sock_poll_wait(file, sk_sleep(socket-ck), wait),获取socket->sock->wq的表头作为wait_address;-->poll_wait(filp, wait_address, p);然后新建一个等待队列的等待项,注册其回调函数为 ep_poll_callback 函数,然后将其放入岛socket->sock->wq的头部;socket接收到数据后会通过该回调函数通知ep对象。
第三件事:将epi插入到ep对象的红黑树;
三epoll_wait,处理IO事件。
查看ep->rdllist有无就绪socket,有就返回;没有就创建一个等待队列项并阻塞该进程,添加到ep对象的ep->wq中,然后自己阻塞等待,当socket接收到数据后会通过该回调函数将就绪socket放入ep->rdllist并且调用eq->wq里default_wake_function唤醒进程。
3.1 判断ep->rdllist有无就绪连接;
3.2 当没有就绪连接时,定义一个等待任务wait_queue_t q,q的private_data指向进程,q的唤醒方法指向default_wake_function;并添加到ep->wq,注意添加时,q->flags |= WQ_FLAG_EXCLUSIVE(互斥);
3.3 让出 CPU,主动进入睡眠状态,阻塞;
3.4 当数据来时,主要完成以下工作:
(1)接收数据到sockt->sock->receieve队列尾部;
具体地,根据数据包header的IP和端口找到对应的socket的sock;
接收数据到sock的接收队列sk_receive_queue。
(2)等数据接收完毕,调用socker->sock->sk_data_ready也就sock_def_readable;
这个函数可以找到epoll_ctl注册到socket->sock->wq等待队列里等待队列项里的注册函数ep_poll_callback;
(3)系统软中断调用 ep_poll_callback,主要完成:
(3.1)基于该等待队列项的base指针获得对应的epitem;
(3.2)基于epi的*ep指针获得对应的eventpoll对象;
(3.3) 将epitem添加到eventpoll->rdllist尾部;
(3.4)查看eventpoll->wq等待队列里是否有等待队列项(epoll_wait 执行的时候会设置,也就是阻塞的用户进程),如果没有则结束;如果有则处理等待队列项 q, 找到epollwait注册到这个等待队列项q里的唤醒方法default_wake_function
(3.5) 调用q的唤醒方法default_wake_function,查到q关联的进程q->private_data,并唤醒之。
完整流程:
socket有数据--->socket->sock->接收队列接收--->接收完毕,socket->sock->sk_data_ready函数唤起调用socket->sock->wq等待队列里等待项目qq里注册的回调函数ep_call_back(epoll_ctl添加socket时放入的sock->wq且注册了ep_call_back)--->基于该等待项目qq获得epi,ep,将epit添加到ep->rdllist尾部--->查看ep->wq等待队列等待项q并调用q的唤醒方法default_wake_function(epoll_wait 执行的时候会设置),查到q关联的进程q->private_data,并唤醒之。