前序
Epoll 是 Linux IO 多路复用的管理机制。 作为现在 Linux 平台高性能网络 IO 必要的组件。所有的linux服务器底层全部用的epoll linux在以前只能做嵌入式设备,但是自从有了epoll的存在,就有了linux可以用来做服务器
1 epoll本身的数据结构
epoll本身只用到了两种数据结构
1 所有的fd总集合 红黑树
epoll和poll的一个很大的区别在于,poll每次调用时都会存在一个将pollfd结构体数组中的每个结构体元素从用户态向内核态中的一个链表节点拷贝的过程,而内核中的这个链表并不会一直保存,当poll运行一次就会重新执行一次上述的拷贝过程,这说明一个问题:poll并不会在内核中为要监听的文件描述符长久的维护一个数据结构来存放他们,而epoll内核中维护了一个内核事件表,它是将所有的文件描述符全部都存放在内核中,系统去检测有事件发生的时候触发回调,当你要添加新的文件描述符的时候也是调用epoll_ctl函数使用EPOLL_CTL_ADD宏来插入,epoll_wait也不是每次调用时都会重新拷贝一遍所有的文件描述符到内核态。当我现在要在内核中长久的维护一个数据结构来存放文件描述符,并且时常会有插入,查找和删除的操作发生,这对内核的效率会产生不小的影响,因此需要一种插入,查找和删除效率都不错的数据结构来存放这些文件描述符,那么红黑树当然是不二的人选。
之所以不用hash结构
hash 缺点 空间浪费 优点:数量很多的情况下,查找效率很高。
2 就绪fd的集合 队列
就绪队列之所以要用队列这种数据结构是秉持着先触发的先调用的原则利用队列的先进先出的优点
epoll 的工作环境
epoll 是已经有了协议栈和vfs都有的前提下面再有的epoll,他是一种解bug解出来的功能
2 协议栈如何与epoll模块通信
select/.poll 与epoll的区别
1 select/poll需要把总集拷贝到内核中,epoll不用
2 实现原理:select/poll 循环遍历总集,是否需要有就绪 ,epoll不需要
1 poll的工作原理:
poll只有一个api,每一次调用poll的时候,需要将fd集合的信息copy到内核中,每次调用都需要。
当然也要从内核中copy到用户态中。poll把总集传入内核中,传出的就是就绪的IO。那么内核如何
知道哪个IO就绪呢?通过循环遍历每一个IO是否就绪。将就绪的IO带出。
而epoll有三个api,它是通过epoll_ctl一个一个的放进内核中。不需要将所有的总集copy到内核中,一旦有新的IO,就通过epoll_ctl将IO加入到红黑树中。一旦有IO触发,就通过epoll_wait 将就绪的IO给带出来。
这个就是poll和epoll设计理念上的区别。
对于io密集型而言,每一次就绪的IO相较于总的IO集合而言,就绪IO的数量是相当少的。
协议栈和epoll通信的时机
如上图所示
1 当三次握手后,服务器的全连接队列(accept队列)中增加一个节点,协议栈会通知到epoll
2 当服务器协议栈接收到数据并且回复确认消息后,也会通知epoll 一个epollin(可写)的消息
3 当snedbuffer中数据以mss为单位发送出去时候,当发送一个mss数据,并且收到这个mss数据返回的ack消息,此时再sendbuffer中将这个mss的数据从sendbuffer中删除,并且协议栈会通知epoll一个epollout(可读的消息)
4 当服务器接收到一个fin标志位的时候,也会通知给epoll一个epollin的消息
5 当有收到rst标志位的时候,协议栈会通知给epoll一个epollerr的信号
协议栈通知epoll的方式
通过回调函数的方式通知epoll
回调函数要做哪些事情呢?回调函数时传入的参数 fd,epollin/epollout/epollerr
1 通过fd查找对应的节点
2 把节点加入到就绪队列
协议栈 通知epoll 和epoll通知用户态两两之间没有耦合,都是异步的关系
协议栈调用回调函数时会调用pthread_cond_signal 通知epoll_wait函数中的pthread_cond_wait(条件等待) 往下面走
内核可以通知用户态,但是用户态不能够通知内核。
3 epoll线程安全如何加锁
epoll 是通过锁来保证线程安全的,多线程同时操作一个epoll,该如何加锁?
1 epoll_ctl是对红黑树加锁
有两种加锁方法:1 对整棵树进行加锁 2锁子树
多个线程对红黑树进行增删改操作时 rbtree_insert rbtree_delete 加锁。
加锁类型:互斥锁
2 epoll_wait 是需要对就绪队列进行加锁
就绪队列就好比生产消费模式。协议栈调用回调函数相当于是生产模式,消费模式就是调用epoll_wait.对于队列而言一般用自旋锁比较合适。应为队列的不安全操作(增删操作较为简单),用粒度比较小的自旋锁更为合适。
互斥锁和自旋锁的区别;对于互斥锁而言当线程没有获取到锁,就会让出线程。而对于自旋锁而言当一个线程没有获取锁就会不断的等待获取到锁。
4 et与lt如何实现
1et 与lt的区别
et 边沿触发:当调用recv函数如果没有把协议栈中的recvbuffr中的数据全部读完,就不会继续触发,当客户端再发数据的时候,才会再次被触发。
lt水平触发:当调用recv函数如果没有把协议栈中的recvbuffr中的数据全部读完,就回一直触发,直到全部读完为止。
2 为什么会有水平触发和边沿触发?
水平触发和边沿触发不是一开始就故意设计出来的,其理念来自于嵌入式的电平的高低变化
如何实现水平触发和边沿触发?
et从协议栈中检测到recvbuffer中接收数据就调用回调,水平触发检测recvbuffer有数据就调回调,平触发和边沿触发代码实现核心是内核通知epoll时执行回调函数的次数的区别。
ET 和 LT 关于sendbuffer的特殊情况。
当sendbuffer为空的时候,ET回一直触发,就和LT的功能很相似。
5 epoll的代码实现主要有两个结构体组成
1 整体的epoll管理
1 rbr:红黑树(总的fd集合)的 根节点
2 rdlist:就绪队列的头节点
3 renum :就绪队列中节点的个数
然后就是线程安全相关的 互斥锁 自旋锁 条件变量
2 epoll 中所谓的节点
存在就绪队列中
1 在红黑树中的节点指针
2 就绪队列中的节点指针
3 是否存在就绪队列中
4 sockfd
5 与sockfd 绑定的相关事件的结构体变量。
若此文章有不妥之处,还请各位大佬能够在评论区加以斧正,感谢!