epoll的数据结构
多种数据结构进行决策
epoll至少需要两个集合
所有fd的总集
就绪fd的集合
那么这个总集选用什么数据结构存储呢?
我们知道,一个fd,其底层对应一个TCB。那么也就是说key=fd,val=TCB,是一个典型的kv型数据结构,对于kv型数据结构我们可以使用以下三种进行存储。
如果使用hash进行存储,其优点是查询速度很快,O(1)。
但是在我们调用**epoll_create()**的时候,hash底层的数组创建多大合适呢?
如果我们有百万的fd,那么这个数组越大越好,如果我们仅仅十几个fd需要管理,在创建数组的时候,太大的空间就很浪费。而这个fd我们又不能预先知道有多少,所以hash是不合适的。
b/b+tree是多叉树,一个结点可以存多个key,主要是用于降低层高,用于磁盘索引的,所以在我们这个内存场景下也是不适合的。
在内存索引的场景下我们一般使用红黑树来作为首选的数据结构,首先红黑树的查找速度很快,O(log(N))。其次在调用epoll_create()的时候,只需要创建一个红黑树树根即可,无需浪费额外的空间。
那么就绪集合用什么数据结构呢,首先就绪集合不是以查找为主的,就绪集合的作用是将里面的元素拷贝给用户进行处理,所以集合里的元素没有优先级,那么就可以采用线性的数据结构,使用队列来存储,先进先出,先就绪的先处理。
所有fd的总集 -----> 红黑树
就绪fd的集合 -----> 队列
epoll的工作环境
应用程序只能通过三个api接口来操作epoll。当一个io准备就绪的时候,epoll是怎么知道io准备就绪了呢?是由协议栈将数据解析出来触发回调通知epoll的。
也就是说可以把epoll的工作环境看出三部分,左边应用程序的api,中间的epoll,右边是协议栈的回调(协议栈当然不能直接操作epoll,中间的vfs在此不是重点,就直接省略vfs这一层了)。
协议栈触发回调通知epoll的时机
socket有两类,一类是监听listenfd,一类是客户端clientfd。对于sockfd而言,我们一般比较关注EPOLLIN和EPOLLOUT这两个事件,所以如果是listenfd,我们通常的做法就是accept。对于clientfd来说,如果可读我们就recv,如果可写我们就send。
协议栈将数据解析出来触发回调通知epoll。epoll是怎么知道哪个io就绪了呢?我们从ip头可以解析出源ip,目的ip和协议,从tcp头可以解析出源端口和目的端口,此时五元组就凑齐了。socket fd — < 源IP地址 , 源端口 , 目的IP地址 , 目的端口 , 协议 > 一个fd就是一个五元组,知道了fd,我们就能从红黑树中找到对应的结点。
那么这个回调函数做什么事情呢?我们传入fd和具体事件这两个参数,然后做下面两个操作
通过fd找到对应的结点
把结点加入到就绪队列
1、协议栈中,在三次握手完成之后,会往全连接队列中添加一个TCB结点,然后触发一个回调函数,通知到epoll里面有个EPOLLIN事件
2、客户端发送一个数据包,协议栈接收后回复ACK,之后触发一个回调函数,通知到epoll里面有个EPOLLIN事件
3、每个连接的TCB里面都有一个sendbuf,在对端接收到数据并返回ACK以后,sendbuf就可以将这部分确认接收的数据清空,此时sendbuf里面就有剩余空间,此时触发一个回调函数,通知到epoll里面有个EPOLLOUT事件
4、当对端发送close,在接收到fin后回复ACK,此时会调用回调函数,通知到epoll有个EPOLLIN事件
我们看到每次调用poll,都需要把总集fds拷贝到内核态,检测完之后,再有内核态拷贝的用户态,这就是poll。而epoll不是这样,epoll只要有新的io就调用epoll_ctl()加入到红黑树里面,一旦有触发就用
epoll_wait()将有事件的结点带出来,可以看到他们的第一个区别:poll总是拷贝总集,如果有100w个fd,只有两三个就绪呢?这会造成大量资源浪费;而epoll总是将需要拷贝的东西进行拷贝,没有浪费
第二个区别:我们从上面知道了epoll的事件都是由协议栈进行回调然后加入到就绪队列的,而poll呢?内核如何检测poll的io是否就绪?只能通过遍历的方法判断,所以poll检测io通过遍历的方法也是比较慢的。
作为 IO 多路复用的一个实现,select 的原理也很简单。所有的 socket 统一保存执行 select 函数的(监视进程)进程 ID。任何一个 socket 接收了数据,都会唤醒“监视进程”。内核只要告诉“监视进程”,那些 socket 已经就绪,监视进程就可以批量处理了。