JAVA多路复用:Selector
Selector是多路复用的基础组件,其可以让一个thread复用多个channel。Selector会不断轮询注册在其中的channel,如果某个Channel上发生了读写事件,这个Channel就处于就绪状态,会被Selector选出并执行后续的IO操作。
Selector模式:当管道注册到Selector后,Selector会为其分配一个key值来标识Channel。当Selector轮询到某个Channel的状态时,会根据相应的key去获取对应的Channel,并执行相关的数据操作。
常见的channel状态有connect,accept,read,write等。
一个简单的案例:
- 服务器创建ServerSocketChannel绑定在selector上。当轮询发现SelectionKey中有accept状态时,会先用
key.channel()
获取ServerSocketChannel并调用ssc.accept()创建SocketChannel,并绑定到selector并设置key为OP_READ。 - 下一次轮询时如果有read数据,就会获取到刚刚的OP_READ与它绑定的SocketChannel,并从channel中获取数据或写入数据。
- 客户端同样也可以创建一个channe到sever并设置key为OP_CONNECT。selector轮询后创建链接,当链接创建成功后注册新的key为OP_READ(绑定的channel不变)。之后就可以在有read数据时获取OP_READ和channel,进而完成通信。
JAVA Selector的核心是subSelector.poll()
方法,其底层调用了native函数poll0来实现。之前的版本是基于select/poll实现的多路复用非阻塞IO,在JDK1.5后底层使用了epoll。
OS多路复用:Select/poll & epoll
select,poll,epoll都是OS底层的多路复用机制。所谓多路复用就是一种机制,通过监听多个文件描述符,一旦某个文件描述符准备就绪,就可以执行相关的读写操作。从系统上讲,这三者都是同步IO。
Select
在select中,读取文件不再有read系统调用,但前提是目标网络连接提前注册到select可查询的socket列表中,即可开启IO多路复用。
- 进行select系统调用,kernel会查询所有注册的socket列表,当有socket准备好时会返回,而此时调用select的程序会被阻塞。(因为select是同步IO)
- 当用户线程获取连接后,发起read调用,内核开始复制数据到用户内存,之后kernel返回结果
- 用户线程接触block状态,用户线程读到程序
如果从更细节的角度看会是这样:
- 系统将用户空间的fd_set拷贝到内核,并注册回调函数_pollwait,进入阻塞状态。
- 内核遍历所有fd,调用其poll方法(sock_poll)拉取数据
- 内核把fd_set重新拷贝回用户空间,证明已经准备就绪(此时select系统调用就结束了)
- 用户线程再调用read方法将数据从内核拷贝到用户空间
可以看到select在调用时,fd_set在用户态和内核态间来回拷贝,当fd过多时会带来很大的开销。同时操作系统最大打开fd默认为1024,存在打开上限。
Poll
Poll和Select实现思路类似,只是使用了pollfd而不是fd_set,进而取消了最大连接数的限制。
Epoll
Epoll是根据select/poll的缺点上的改进,主要包含epoll_create,epoll_ctl和epoll_wait三个函数。
epoll使用红黑树来监听并维护所有的fd。
- 在调用epoll_create时,内核会创建红黑树来存储之后epoll_ctl传来的socket,并创建一个准备就绪的链表。
- 当epoll_wait被调用时,仅需要观察链表是否为空即可,可以看到轮询不在遍历所有的fd而是观察就绪链表是否为空。只需要将内核态准备就绪的fd拷贝回用户态即可。
- 可以看到整个fd和就绪链表都是由epoll_ctl来维护的。当socket放到红黑树后,会在内核中断处理函数中注册一个回调函数。该回调函数会在socket数据到(即内核已经有数据)后,将该socket放入到就绪链表中。
可以看到Epoll减少了内核态到用户态间频繁的fd拷贝,使用了mmap将用户空间和内核映射同一片区域。同时引入了红黑树管理fd,使用了回调函数等机制,都让epoll的性能比select/poll要好很多。