为什么要在用户态协议栈去实现一个epoll?
内核里面实现的epoll是对sockfd,文件系统 vfs进行的一个管理。
而对于用户空间的fd, (int类型的fd),内核里的epoll是用不了的,不好管理,于是可以自己去实现一个epoll。包括腾讯的f-stack,也实现了一个epoll。
实现一个用户态的epoll需要解决的问题
1.如何定义数据结构
2.协议栈如何与epoll模块通信
3.epoll如何加锁
4.ET与LT如何实现
epoll对外提供epoll_create(),epoll_ctl(),epoll_wait()这三个接口
一、如何定义数据结构
服务器管理了大量的fd,至少包含2个集合,需要包含哪些集合呢
1.所有fd总集(红黑树)
2.就绪的fd集合(队列)(不用于查找,所有的都需要处理,也没有key-value关系,可以使用线性数据结构。用队列比较合适,通过epoll_wait把对头部的取出来。如果用stack可能导致,部分数据一直都取不出来)
用什么数据结构去存呢?
是一个key-value的格式,可以通过fd对找到对应的value,有以下几种可能
1.hash(缺点:空间浪费,优点:当fd数量比较多的时候,查找性能比较优)
2.数组(空间不好扩展,查找速度慢)
3. 红黑树
4. b树/b+树是个N叉树,层高比较低,但是对比次数不比红黑树少)(对于一个节点查找下一个节点时间比较长的情况比较使用,降低层高,常用在磁盘上)
所以一般可以选择:红黑树 (在时间和空间综合考虑下,时间是最优的)
select/poll与epoll区别
1.使用;select/poll需要把总集拷贝到内核,而epoll不用
2.实现原理:select/poll,循环遍历总集合,检查是否有就绪的。而epoll是哪个节点,就把它加入到就绪队列
二、协议栈如何与epoll模块通信
epoll如何检测到是哪个节点呢?
1.在三次握手之后,tcb会加入全连接队列,此时服务器协议栈会通知epoll,后续通过accept处理通知
2.当客户端发送信息时,服务端协议栈接收到信息后,会回复一个ack,然后通知epoll,也就是EPOLLIN事件,后续通过recv来接受数据
3.当服务端的sendbuffer不为空的时候,服务器会向客户端发送数据,然后当客户端回送一个ack时,然后回通知epoll(EPOLLOUT),后续通过send来发送数据
4.接收到fin标志位的时候,也会通知epoll(EPOLLIN),后续通过close来关闭fd
5.接受到rst标志位的时候,会通知epoll(EPOLLERR)
那是epoll如何检测到是哪个fd的事件触发了呢?
根据 协议栈接收到的五元组—>查找到fd---->根据fd去红黑树找到相应的节点
如何将总集中的节点和红黑树中的节点关联起来呢?
就绪队列中的节点和 总集(红黑树,用来管理全部fd)中的节点是同一个,只不过,将它们连接了起来。如果要在就绪队列中删除一个节点,只要将它们的节点断开就行了。
struct epitem
就是节点
rbn
表示红黑树的节点,rdlink
表示就绪队列的节点
struct epitem { RB_ENTRY(epitem) rbn; LIST_ENTRY(epitem) rdlink; int rdy; //exist in list int sockfd; struct epoll_event event; };
协议栈如何通知epoll呢?
从协议栈回调(通过回调函数 )到epoll模块
1.通过fd查找对应的节点
2.把节点加入到就绪队列
epoll要实现的功能
1.epoll_create
创建一个红黑树的根节点
2.epoll_ctl
,ADD,DEL, MOD
3.epoll_wait
把就绪就绪队列里面的节点copy到用户空间
协议栈如何与epoll模块通信?
是异步的,没有耦合
三、epoll如何加锁
(为什么要加锁:为了使得线程安全,需要加锁)
对一颗红黑树操作的情况下
epoll_ctl
时 对 红黑树加锁 (使用互斥锁)
epoll_wait
时 对 就绪队列加锁(使用自选锁)
互斥锁和自旋锁使用的区别:
互斥锁在没拿到锁的时候会让出cpu,而自旋锁不会,它会一直等着,考虑到等待一段时间的成本更低的情况可以选择使用自旋锁。因此队列用自旋锁更加合适
四、ET与LT如何实现
1.et接受到数据,就调用一次回调
2.lt 检测recvbuffer里面有数据,就调用回调
回调要做的时候,就是查找fd,加入到就绪队列中。如果要加入的时候已经在就绪队列中了,就不需要去管了(不用再加入了)。
struct eventpoll { ep_rb_tree rbr;//红黑树的头节点 int rbcnt;//红黑树节点的个数 LIST_HEAD( ,epitem) rdlist;//就绪队列的起点 int rdnum;//就绪队列的节点数量 int waiting; pthread_mutex_t mtx; //rbtree update pthread_spinlock_t lock; //rdlist update pthread_cond_t cond; //block for event //条件等待,在epoll_wait的时候第四个参数为-1,会一直等待。 每触发一次回调就调用一次signal pthread_mutex_t cdmtx; //mutex for cond };
epoll_wait
进行条件等待,当协议栈通知epoll事件的时候,就调用一次signal
,使得epoll_wait
往下走