IO多路复用需要OS的支持,IO多路复用模型中,引入了一种新的系统调用,查询IO的就绪状态。
在Linux系统中,JavaNIO的Selector#select()
方法对应的系统调用为select/epoll系统调用。
通过该系统调用,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核能够将就绪的状态返回给应用程序。随后,应用程序根据就绪的状态,进行相应的IO系统调用。
目前支持IO多路复用的系统调用,有select、epoll等等。select系统调用,几乎在所有的操作系统上都有支持,具有良好的跨平台特性。epoll是在Linux 2.6内核中提出的,是select系统调用的Linux增强版本。
在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程,可以不断地轮询成百上千的socket连接,当某个或者某些socket网络连接有IO就绪的状态,就返回对应的可以执行的读写操作。
IO多路复用模型的特点:
IO多路复用模型的IO涉及两种系统调用(System Call),一种是IO操作,另一种是select/epoll(就绪查询)。
IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll。
多路复用IO也需要轮询。负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,查找出达到IO操作就绪的socket连接。IO多路复用模型与同步非阻塞IO模型是有密切关系的。
对于注册在选择器上的每一个可以查询的socket连接,一般都设置成为同步非阻塞模型。仅是这一点,对于用户程序而言是无感知的。
IO多路复用模型的优点:
与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接(Connection)。系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。Java语言的NIO(New IO)技术,使用的就是IO多路复用模型。在Linux系统上,使用的是epoll系统调用。
IO多路复用模型的缺点:
本质上,select/epoll系统调用是阻塞式的,属于同步IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个读写过程是阻塞的。
⭐️注意:轮询的效率比较低,在硬件和操作系统层有个概念叫做“中断”, 这种机制可以在设备准备好的情况下,主动通知等待的程序。
Selector selector = Selector.open();
调用栈如下,在我的笔记本上open()
方法就是返回一个WindowsSelectorImpl
实例,在其构造方法中调用了一些native
方法。
<init>:126, WindowsSelectorImpl (sun.nio.ch)
openSelector:44, WindowsSelectorProvider (sun.nio.ch)
open:227, Selector (java.nio.channels)
startServer:63, NioReceiveServer (com.yh.stu.nio.socket)
main:177, NioReceiveServer (com.yh.stu.nio.socket)
// 2、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4、绑定连接
ServerSocket serverSocket = serverSocketChannel.socket();
InetSocketAddress address
= new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_PORT);
serverSocket.bind(address);
// 5、将通道注册到选择器上,并注册的IO事件为:“接收新连接”
Print.tcfo("serverSocketChannel is linstening...");
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
ServerSocketChannel.open()
创建一个ServerSocketChannel
对象(实际类型是 ServerSocketChannelImpl)对象,如下图
# new ServerSocketChannelImpl(SelectorProviderImpl)
openServerSocketChannel:56, SelectorProviderImpl (sun.nio.ch)
open:108, ServerSocketChannel (java.nio.channels)
run:120, PipeImpl$Initializer$LoopbackConnector (sun.nio.ch)
run:76, PipeImpl$Initializer (sun.nio.ch)
run:61, PipeImpl$Initializer (sun.nio.ch)
doPrivileged:-1, AccessController (java.security)
<init>:171, PipeImpl (sun.nio.ch)
openPipe:50, SelectorProviderImpl (sun.nio.ch)
open:155, Pipe (java.nio.channels)
<init>:127, WindowsSelectorImpl (sun.nio.ch)
openSelector:44, WindowsSelectorProvider (sun.nio.ch)
open:227, Selector (java.nio.channels)
startServer:63, NioReceiveServer (com.yh.stu.nio.socket)
main:177, NioReceiveServer (com.yh.stu.nio.socket)
serverSocketChannel.socket()
方法,在ServerSocketChannel
中创建一个SocketChannel
对象 (实际类型是ServerSocketAdaptor)
socket:110, ServerSocketChannelImpl (sun.nio.ch)
createServerSocketChannel:110, NioReceiveServer (com.yh.stu.nio.socket)
startServer:65, NioReceiveServer (com.yh.stu.nio.socket)
main:177, NioReceiveServer (com.yh.stu.nio.socket)
serverSocket.bind(address)
方法最终调用的是 ServerSocketChannel
的 bind(..)
方法,调用栈和示例图如下:
bind:220, ServerSocketChannelImpl (sun.nio.ch)
bind:74, ServerSocketAdaptor (sun.nio.ch)
bind:67, ServerSocketAdaptor (sun.nio.ch)
createServerSocketChannel:113, NioReceiveServer (com.yh.stu.nio.socket)
startServer:65, NioReceiveServer (com.yh.stu.nio.socket)
main:177, NioReceiveServer (com.yh.stu.nio.socket)
调用 WindowsSelectorImpl
父类 SelectorImpl
的 register
方法将进行注册,源码如下:
//sun.nio.ch.SelectorImpl#register
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment)
{
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
//将`Selector` 和`ServerSocketChannel` 包装成了 `SelectionKeyImpl `对象
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
k.attach(attachment);
synchronized (publicKeys) {
implRegister(k);
}
k.interestOps(ops);
return k;
}
// sun.nio.ch.WindowsSelectorImpl#implRegister
protected void implRegister(SelectionKeyImpl ski) {
synchronized (closeLock) {
if (pollWrapper == null)
throw new ClosedSelectorException();
growIfNeeded();
channelArray[totalChannels] = ski;
ski.setIndex(totalChannels);
fdMap.put(ski);
keys.add(ski);// 将 `SelectionKeyImpl ski` 放入`HashSet` 中
pollWrapper.addEntry(totalChannels, ski);
totalChannels++;
}
}
从源码中不难看出,将Selector
和ServerSocketChannel
包装成了 SelectionKeyImpl
对象,并将 SelectionKeyImpl
放入Set
中,同时SelectionKey.OP_ACCEPT
也
其实在ServerSocketChannel
中也有一个数组,用来放 SelectionKey
的引用,方法调用栈和图示如下
addKey:115, AbstractSelectableChannel (java.nio.channels.spi)
register:213, AbstractSelectableChannel (java.nio.channels.spi)
register:280, SelectableChannel (java.nio.channels)
startServer:66, NioReceiveServer (com.yh.stu.nio.socket)
main:177, NioReceiveServer (com.yh.stu.nio.socket)
···
到这里一切准备就绪了,等待客户端来连接
调用栈如下
<init>:129, SocketChannelImpl (sun.nio.ch)
accept:266, ServerSocketChannelImpl (sun.nio.ch)
createSocketChannel:97, NioReceiveServer (com.yh.stu.nio.socket)
startServer:78, NioReceiveServer (com.yh.stu.nio.socket)
main:177, NioReceiveServer (com.yh.stu.nio.socket)
SocketChannelImpl(SelectorProvider sp,
FileDescriptor fd, InetSocketAddress remote)
throws IOException
{
super(sp);
this.fd = fd;
this.fdVal = IOUtil.fdVal(fd);
this.state = ST_CONNECTED;
this.localAddress = Net.localAddress(fd);
this.remoteAddress = remote;
}