杂学Linux-IO篇(二)

导读

这篇文章是我学习IO的第二篇文章,这里接着上篇的BIO模型说起,在学习了上一篇的BIO后我们应该明白BIO最大的特色就是阻塞,每个连接、每个请求都是阻塞式的调用。但随着互联网的快速发展,阻塞式的IO会极大的影响系统的性能,为了满足高并发、快速响应的需求,我们对IO有了更近一步的要求,出现了NIO,也就是新的IO模型。

NIO

什么是NIO

NIO是建立在BIO基础上的一种新的IO模型,它是对BIO最大特色阻塞式的IO的一种改进,它采用非阻塞式的方式来提升性能,简单的理解就是它是非阻塞式的IO,连接的过程以及通信的过程都是非阻塞式的,再也不用阻塞的等待请求或等待消息的到来。

用代码的形式来解读的话就是在BIO模型下,等待连接时我们会调用accept方法来阻塞的等待着客户端的连接到来,如果没有连接那么程序会一直阻塞在accept方法中,如果一个连接到达,那么我们会克隆出一个新的线程去和这个请求客户端建立连接,每有一个连接就会克隆一个新线程,而我们的等待连接线程会再次回到accept方法中阻塞. 但是在NIO模型下,我们完全可以将等待连接的代码与接收发送消息的代码放在一个线程中,让它一直while循环即可完成连接与通信的任务。再也不需要每有一个客户端连接就新开一个线程.

我么也可以说NIO在JDK层面是新IO,而在操作系统层面是NONBLOCKING IO.
在这里插入图片描述


NIO的演进之路

前置知识

在谈NIO的演进之前我先在这介绍一点操作系统方面的知识:

  1. IO的过程涉及用户态和内核态的切换,切换不仅消耗时间还消耗资源,每一次处理连接、接受或发送消息都会有用户态与内核态的切换。
  2. 每次此的用户态与内核态的切换都是一次系统调用。

起步阶段

NIO的起步阶段解决的时BIO的阻塞问题,在BIO模型下,每一个客户端连接都需要开辟新的线程来维持双方的通信,而起步阶段的NIO解决了这个问题,它能够使用1个或有限的几个线程来处理N个客户端的连接与通信。

多路复用器阶段

问题: 虽然起步阶段的NIO实现了用有限的线程来解决多连接的问题(C10K问题),但是它还存在的一个问题就是每维持一个连接,那么它就得一直轮询的去做系统调用,看是否有数据到达,去读取数据,假设有10000个连接,那么在我们的处理线程中每次都得轮询的进行10000次的系统调用,看哪个连接有消息到了,可以读了(这还只是一次轮询,我们的轮询需要一直进行)。
如下图中的左图,我们的app需要轮询查看哪个IO有数据到了,哪个可以读数据了。
在这里插入图片描述
为了解决这个问题我们引入了多路复用器,多路复用器又有select的、poll的与epoll的,不同的操作系统有不同的实现,抽象的描述就是上图中右边的图示,我们在app与内核之间建立了一个多路复用器。

任何的多路复用器只是完成状态的判断,读数据还是需要进行系统调用。

select&poll型的多路复用器

使用select或者poll多路复用器之后,当我们维持有10000个连接时,在我们的处理线程中每次轮询时我们总是会一次将这10000个连接的FD传给内核,让内核来完成轮询(完成状态判断,是否可读,即是否有数据到达),内核会把有数据到达的FD做一个记录然后一起返回给线程,接着线程去轮询的读取有数据的FD。

解决的问题

在没引入多路复用器之前,加入有N个连接,那么每进行一次判断需要进行N次的轮询系统调用,而引入select或poll之后,我们只需要进行m+1次即可,减少了系统调用的次数。

注:

  1. m+1中的1为发送所有的FD给内核,让内核去判断哪些可读,m为内核判断到可读的那些FD,我们的线程要进行m次的数据读取。
  2. 这种方式减少的系统调用次数即那些没有消息传来的连接。
epoll型的多路复用器

epoll时建立在select与poll的基础上的,它时对上面两种多路复用器的改进。这先来看看上面的两种多路复用器有什么缺点,epoll又是如何改进的。

问题
  1. 线程需要多次大量的FD拷贝给内核,每次轮询线程都得重新重复拷贝所有的FD给内核。
  2. 内核每次都得轮询全量的检查FD。
解决方法
  • 针对第一个问题,我们开辟一块内存空间来存储FD,这样就不需要进行重复的数据拷贝。在epoll中我们会使用epoll_creat建立一快内存空间,使用epoll_ctl将所有的FD以红黑树的结构保存在开辟的内存空间中。
  • 对于第二个问题,epoll在操作系统层面做了改进,在操作系统中,当网卡收到消息后会触发中断,默认的中断回调函数会把数据直接写到对应的FD中,而epoll对回调函数做了改进,它除了会将数据写入对应的FD中,还会在新建的FD红黑树中找到这个FD并将其写到一个链表中,这样内核也就不用每次去轮询检查所有的FD,在需要的时候直接将该链表返回即可,在我们的线程中直接调用epoll_wait即可拿到该链表。
多路复用器解决的问题
  1. 减少系统调用的次数。
  2. 减少轮询的判断次数。

Java中多路复用器的API

上面说的这些都是操作系统层面的一些东西,在Java层面Selector是对这些东西的封装抽象。

Java中Select的一些API:
            private ServerSocketChannel server = null;
            //linux 多路复用器(select poll epoll) nginx  event{}
            private Selector selector = null;   
            int port = 9090;
            //相当于new一个 ServerSocket
			server = ServerSocketChannel.open();
			//设置非阻塞式
            server.configureBlocking(false);
            //绑定端口
            server.bind(new InetSocketAddress(port));
            //多路复用器的一些方法
            selector = Selector.open();  //  select  poll  epoll
            //在Select上注册监听事件
            server.register(selector, SelectionKey.OP_ACCEPT);
			//去Select上获取FD状态 如果返回值大于0则有FD状态发生变化
			selector.select();
			//从Select中拿到事件集合
			Set<SelectionKey> selectionKeys = selector.selectedKeys();
			//获取事件集合迭代器,可按照迭代器的方式遍历集合,进行FD操作
            Iterator<SelectionKey> iter = selectionKeys.iterator();
            SelectionKey key = iter.next();
		   	//判断这个FD是请求建立连接的状态吗
		    key.isAcceptable();
		    //判断这个FD是可读的状态吗
		    key.isReadable();
		    //判断这个FD是可写的状态吗
		    key.isWritable();
		    //取消这个FD的事件,即从红黑树里删除这个FD
		    key.cancel();         
API理解

在这里插入图片描述

NIO中引入Selector的使用理解:

1.Selector是一个监控IO状态独立的组件,没有它我们的IO也可正常使用,引入它只是为了提升IO的效率;

2.在使用Selector多路复用器之后,我们通常会将ServerSocketChannel与SocketChannel类对象注册在Selector上,在使用时会调用Selector的select()来获取IO的状态(获取到许多SelectionKey类的对象),如果我们注册的是ServerSocketChannel的对象,那么我们可以从获取到的对象中拿到一个ServerSocketChannel,例如: ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); 如果我们注册的是SocketChannel类对象,那么我们从获取到的对象中拿到的就是一个SocketChannel ,例如:SocketChannel client = (SocketChannel) key.channel();

3.如果我们在注册ServerSocketChannel的时候,同时传入一个ByteBuffer,那么我们也可以从获取到的对象中拿到这个ByteBuffer ,例如:ByteBuffer buffer = (ByteBuffer) key.attachment();

NIO中引入Selector的使用过程描述:

1.有ServerSocketChannel或SocketChannel对象与Selector组件;
2.注册ServerSocketChannel或SocketChannel对象到Selector组件中;
3.Selector负责监听注册的事件;
4.轮询获取Selecotor的SelectionKey(Selector的select方法放回一个个的SelectionKey对象),取出、判断类型、处理;

多路复用器的理解

多路复用器的使用有种事件机制的思想,我们可以将某个IO的等待连接、判断可读、可写当成一个个的事件,当需要用到多路复用器时我们将需要它为我们做的事情以事件的方法注册到它上(通过register方法),当不需要的时候可以取消事件(通过cancle方法)。

单线程&多线程多路复用器

多路复用器的引入极大的提高了系统的IO性能,在单线程中我们就可以维持许多的连接,但单线程能维持的连接毕竟是有限的并且单线程也不能使资源得到充分的利用,因此我们在实际的生产环境中用到的多路复用器大多数还是在多线程的环境中,但是的后面还有但是,多线程的引入会使得多路复用器出现一些其他的问题,下面我们对这些问题做个简单的描述。

问题

引入多线程我们的IO过程变成了:主线程负责轮询获取所有的FD的状态,拿到后判断是连接的、可读的、还是可写的,然后具体的连接、读与写交给其他的线程处理。

这种情况下就会出现一些问题,举个简答的例子:当主线程拿到一个可读的状态后交给新的线程来处理具体的读任务,它立即就进入下一轮的轮询,此时读线程还没有结束(只有读任务处理完成,FD的状态才会发生改变),那么主线程就有可能再次拿到这个FD的可读,就会再新建一个线程来处理读任务,这就造成了多次读的错误。

解决方法:
1.在主线程拿到FD的状态之后,先取消这个FD的状态事件,然后再新建一个线程去处理具体的状态任务,当处理完之后再重新注册事件,这就避免了上述问题的发生,但这种方式也多了系统调用的次数,每次注册与取消事件都会发生系统调用,因此我们有了下面的这种解决方式。
2.我们创建多个Selector(多路复用器),将FD分组分配到不同的多路复用器上,每个线程一个多路复用器,每个多路复用器负责一组FD,每个多路复用器里以单线程的方式去处理IO,从整体来看多个多路复用器并行处理IO,也就意味着IO是多线程并行处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值