IO复用
IO=等待+拷贝
IO复用是一种通过减少等待时间,来提高IO效率的方式。 其原理是通过同时管理多个IO接口(文件描述符),将等待的时间重叠,这样使得在相等的时间内,出现满足条件的文件描述符的概率增大。
左图是一般的阻塞/非阻塞IO,右图是IO复用
很明显看出,IO复用的单位时间内文件描述符准备好的概率更大,这就代表其单位时间内等待准备的时间更短,所以IO更高效
认识epoll
epoll是一种通过IO复用来提高IO效率的方式,是基于select、poll的提升版本,其克服了select、poll的主要缺陷
1 | 2 | 3 | 4 | |
---|---|---|---|---|
select | 同时等待的fd有上限 | 输入输出参数混合,每次都要重新设定 | 内核和用户之间的数据拷贝多 | 底层监视多个fd时,OS会在内部进行遍历检测 |
poll | null | null | 内核和用户之间的数据拷贝多 | 底层监视多个fd时,OS会在内部进行遍历检测 |
epoll | null | null | null | null |
下图是《Linux高性能服务器编程》中对三种多路转接的方式的比较
epoll原理
谈论epoll的原理离不开3点:
- 红黑树
- 就绪队列(双向链表)
- 回调机制
-
创建实例
- 调用epoll_create()创建一个eventpoll实例,其内部存储了红黑树根节点、双链表等重要结构。
-
事件注册
-
当使用epoll_ctl函数向epoll实例中添加需要监视的文件描述符时,可以指定对该文件描述符感兴趣的事件类型(如可读、可写等)。
-
在这个过程中,epoll会在内核中创建一个epitem结构体来表示这个监视关系,并将该结构体插入到红黑树中,以便快速查找和更新。
-
-
事件就绪检测
- 当文件描述符上的事件发生时(如数据到达socket缓冲区),内核会检测到这一变化,并触发相应的处理逻辑(回调机制)。
- epoll使用回调函数来处理这些就绪事件。这些回调函数是内核在检测到事件时自动调用的,而不是由用户显式调用的。
-
事件通知
- 当回调函数被调用时,它会将就绪的文件描述符从红黑树中取出,并添加到就绪链表中(实际上并非取出,而是更改其内部的双链表指针,这是Linux独特的双链表结构,使用强转和位偏移可以找到对象任意位置)。
- 用户进程通过调用epoll_wait函数来等待和接收就绪事件。epoll_wait函数会阻塞用户进程,直到有就绪事件发生或超时(可以设置为非阻塞)。
- 当epoll_wait返回时,它会将就绪链表中的事件复制到用户提供的数组中,并返回就绪事件的数量。
重要结构体
// epoll实例,调用epoll_create创建的结构体
struct eventpoll {
spinlock_t lock;
struct mutex mtx;
wait_queue_head_t wq;
wait_queue_head_t poll_wait;
struct list_head rdllist;
struct rb_root rbr;
struct epitem *ovflist;
struct user_struct *user;
};
// 红黑树节点
struct epitem {
struct rb_node rbn;
struct list_head rdllink;
struct epitem *next;
struct epoll_filefd ffd;
int nwait;
struct list_head pwqlist;
struct eventpoll *ep;
struct list_head fllink;
struct epoll_event event;
};
struct rb_node
{
unsigned long rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
struct list_head {
struct list_head *next, *prev;
};
上图是Linux2.6源码中调用epoll_ctl创建新的文件描述符时创建的节点结构体
struct rb_node rbn
:存储该节点的颜色、左右孩子、以及父亲节点(这里rb_node结构体中虽然没有用rb_node*来存储父节点,但是通过rb_parent_color可以推断出,Linux为了节省存储开销,将父节点的地址和颜色结合在了一起(联合字段))struct epoll_event event
:存储该节点对应的事件struct list_head rdllink
:指向双链表struct eventpoll *ep
:指向其所属的eventpoll对象struct epoll_filefd ffd
:事件句柄信息、文件描述符
红黑树的作用
利用红黑树的平衡原理,使得插入、删除、查找变得高效。当一个文件描述符状态发生变化时(硬件能够检测到),可以高效的遍历到该文件描述符所对应的节点,进而对其属性进行修改。
就绪队列
就绪队列采用双链表,将所有准备就绪的文件描述符链入该队列中,确保了用户读取使用的时候,无需进行任何遍历,直接按顺序recv就行。
回调机制
当你向计算机内输入数据的时候(无论键盘还是网络),计算机的中断机制都会将该数据读取到,然后告知上层。在epoll中,可以提前设置回调函数,当网络中来数据时,OS会发现,并调用该回调函数,然后就可以修改红黑树中的节点状态并将该节点链入就绪队列中。
epoll的ET、LT
LT(Level Trigger 水平触发-epoll默认方式) 、ET(Edge Trigger 边沿触发)
LT:当文件描述符准备好后,只要该文件描述符没有进行处理,那么epoll_wait()就会一直响应该文件描述符,告诉用户你要进行处理了,直到你读取了文件描述符内的数据
ET:只有当文件描述符首次准备好、以及后续有新的内容到达时,epoll_wait()才会通知用户读取数据。一旦你没有进行处理,同时后续也没有新的数据到达,那么该文件描述符内的数据也就读不到了。
注意!!ET模式下,所有的文件描述符都需要是非阻塞状态
- 避免阻塞在epoll_wait上
- 确保数据读取的完整性
由于上述原因,所以处于ET模式下的epoll要求:程序员必须马上把我文件描述符内的数据读完,不然你可能就再也读不到了。当然处理LT模式下的也可以这么做,但是由于LT不是硬性要求赶快读完,所以就有容错空间。
epoll高效的原因
这里只对epoll相较于select、poll的高效作原因分析
- select、poll的fd都是用户态设置的,所以会存在多次的用户内核态切换。而epoll的fd是常驻内核的,减少了状态的切换和拷贝。
- select、poll只支持LT模式, 而epoll还支持ET模式。在ET模式下,epoll会尽快接收完报文,然后较快的返回TCP应答报文中的窗口字段值,这样发送方就可以更快、更多的发送数据。
- select、poll在检测文件描述符状态的时候,都需要遍历fd。而epoll不需要,大大减少了遍历带来的开销。
epoll需要解决的问题
如何保证数据读取的完整性?
这种问题普遍存在于多路转接中:
当某个文件描述符已经准备好了,调用recv读取数据的时候,发现这个文件描述符内的内容并不完全,此时如果再去调用别的文件描述符,那么原先缓冲区的内容就会被覆盖,原先读到的内容也没了
解决方案:Reactor模式
原理:为每一个文件描述符设置一个缓冲区,在上层通过协议来确保读取到的数据是完整的
对于使用epoll的建议
epoll这么好,是不是所有的服务都要使用epoll呢?不是!
- epoll虽然好,但是其体量较大,对于一些嵌入式设备,开销和收益(带来的性能提升)不成正比
- epoll出来的较晚,并非所有的系统都兼容epoll,而select出现较早,兼容性较好
不管怎样,epoll的性能优势还是很大的,在具体情况下还是需要具体分析来判断使用epoll还是select