IO知识
缓冲IO:3次拷贝
直接IO:2次拷贝
内存映射:只剩下一次内存到磁盘文件的拷贝了。
零拷贝技术:是指socket通信,在内存中零拷贝,但是仍然有磁盘到内存、内存到网络IO的拷贝。
网络IO模型
同步异步是针对读写操作由谁来完成,阻塞非阻塞是从函数调用者的角度来说是否需要等待。
同步阻塞IO、同步非阻塞IO、IO多路复用(select、poll、epoll)、异步IO。
select和poll传递fd时,会在内核态和用户态之间切换,比较耗时。
NIO
Selector维护着一个SelectionKeys是一个Set容器里面存放着所有连接注册进来的客户端,每次Selector监听时其实就是查看SelectionKeys容器所有连接有没有事件,有事件发生就会提取出来装起来,最后返回一个装有事件的SelectionKey容器,通过遍历容器获取对应的channel继续业务操作。
参考:详细JAVA-NIO介绍与使用_JolyouLu的博客-CSDN博客
NIO和epoll的关系
NIO提供了对IO的读写操作,操作对象可以是磁盘文件,也可以是socket,而epoll是指往往指网络IO。nio具体读写socket事件怎么来的,可以通过epoll来完成。
NIO和IO多路复用是独立的,两个不一定要一起使用,通过线程池一样可以做到NIO,但是我要去轮询所有的sockett连接,性能太差。
IO多路复用,减少了用户态和内核态切换,因为网络IO在内核态处理。
select将多次内核态和用户态的切换,改为一次,因为它一次性把所有要检查的socket连接都丢给了内核,而不是一个一个丢。
epoll也只是IO多路复用的一种。
参考:NIO核心原理深度解析 - 知乎 (zhihu.com)
两种模式
Reactor模式:用户线程会不断的去轮询IO是否就绪,由用户线程来完成读写。
Proactor模式:读写交给操作系统或网络框架来完成。
高效的三大要素
mmap:内存映射,没有内存拷贝,消除了内核态和用户态之间的频繁切换。
红黑树:epoll 内部使用红黑树来保存所有监听的 socket,红黑树是一种平衡二叉树,添加和查找元素的时间复杂度为 O(log n),保证了插入、删除、查找的性能。
链表:一旦有事件发生,epoll就会将该事件的回调函数(回调函数将就绪事件添加到rdllist双向链表中,而不是采用轮询方式,时间复杂度降低到o(1),参考:redis中epoll事件怎样与读写回调函数绑定_makesifriend的博客-CSDN博客_epoll 回调函数)添加到双向链表中。
为什么要使用红黑树?
1、哈希表. 空间因素,可伸缩性.
(1)频繁增删. 哈希表需要预估空间大小, 这个场景下无法做到.
间接影响响应时间,假如要resize,原来的数据还得移动.即使用了一致性哈希算法,
也难以满足非阻塞的timeout时间限制.(时间不稳定)
(2) 百万级连接,哈希表有镂空的空间,太浪费内存.
2、跳表. 慢于红黑树. 空间也高.
3、红黑树. 经验判断,内核的其他地方如防火墙也使用红黑树,实践上看性能最优.
工作流程
epoll_create:新建epoll描述符。新版本把哈希表改成了红黑树,所以传入的size也无用了。
epoll_ctl:添加或者删除所有待监控的连接。当把事件添加进来的时候时候会完成关键的一步,那就是把该事件与相应的设备(网卡)驱动程序建立回调关系;当相应的事件发生后,就会调用这个回调函数;通过回调函数将就绪事件添加到rdllist双向链表中。不再采用忙轮询的方式,时间复杂度降低到o(1)。
epoll_wait:根据rdlist返回的活跃连接。
与select相比,epoll分清了频繁调用和不频繁调用的操作。
触发模式
LT水平触发:写的死循环,缓冲区写满的概率很小,所以写的条件会一直满足。当用户写完数据时,记得取消写事件。
ET边缘触发:短读,只读取了一部分数据,剩下的没有读,也不会再次触发。
一般推荐默认的LT模式(比如,java nio),虽然有少许的性能损耗,但是写起来很安全。ET容易漏事件,一次没有处理好,就没有第二次机会了。
参考:Linux下的I/O复用与epoll详解 - junren - 博客园 (cnblogs.com)
Netty
netty最流行的 NIO 框架,对nio进行了封装。并发高、传输快、封装好。
在Netty中,NioChannel体系是水平触发,EpollChannel体系是边缘触发。
参考:Netty入门教程——认识Netty - 简书 (jianshu.com)