在讨论IO的时候,参与者通常有两个角色:系统内核和用户进程。用户进程发送 IO请求过后,系统内核在准备好IO数据后,会通过内存拷贝的方式,将准备好的缓存IO数据共享给用户进程缓存。
调用InputStream.read()或者OutputStream.write()时,用户进程会阻塞住直到数据就绪,相当于一线程一连接的方式。所以在采用Java IO时,在Server端通常会采用对于每个新连接,起一个新的线程去处理,这样后来的连接就不用等到之前的完成才能操作。但也带来了问题,毕竟线程是系统的稀缺资源,数量上会有瓶颈,达到一定数量后,性能急剧下降,内存崩溃。不能应对大量连接的情况,而且线程切换很耗费系统资源。
基于Java IO的缺点,NIO采用了新的设计方式,核心在Channel,Buffer,Selector。非阻塞主要依靠Selector,Channel在Selector上注册自己感兴趣的事件,然后Selector线程会轮询注册在自己身上的Channel,当有数据准备就绪时,就通知相应的Channel。这样一个Selector可以管理多个Channel,但实际上还是阻塞的,现在不阻塞IO层面了,阻塞在Selector线程上了。而且采用轮询的方式,效率比较低。
在Java NIO的基础上,增加了AsynchronousChannelGroup,CompletionHandler,其中AsynchronousChannelGroup起到了事件收集和任务分发的作用,而CompletionHandler是绑定在事件上回调机制,从而达到异步。能否真正实现异步,关键还要看系统底层的实现,当前来看只有window的iocp实现了真正的异步,linux上还是通过epoll来模拟,是一种伪异步。
Select/Poll, epoll/kqueue, iocp
select模型
1. 最大并发数限制,因为一个进程所打开的FD(文件描述符)是有限制的,由FD_SETSIZE设置,默认值是1024/2048,因此Select模型的最大并发数就被相应限制了。自己改改这个FD_SETSIZE?想法虽好,可是先看看下面吧…
2. 效率问题,select每次调用都会线性扫描全部的FD集合,这样效率就会呈现线性下降,把FD_SETSIZE改大的后果就是,大家都慢慢来,什么?都超时了??!!
3. 内核/用户空间 内存拷贝问题,如何让内核把FD消息通知给用户空间呢?在这个问题上select采取了内存拷贝方法。
基本上效率和select是相同的,select缺点的2和3它都没有改掉。
把其他模型逐个批判了一下,再来看看Epoll的改进之处吧,其实把select的缺点反过来那就是Epoll的优点了。
1. Epoll没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于2048, 一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。
2. 效率提升,Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
3. 内存拷贝,Epoll在这点上使用了“共享内存”,这个内存拷贝也省略了
首先application调用 recvfrom()转入kernel,注意kernel有2个过程,wait for data和copy data from kernel to user。直到最后copy complete后,recvfrom()才返回。此过程一直是阻塞的
可以看见,如果直接操作它,那就是个轮询。。直到内核缓冲区有数据。
I/O multiplexing (select and poll)
select先阻塞,有活动套接字才返回。与blocking I/O相比,select会有两次系统调用,但是select能处理多个套接字。
signal driven I/O (SIGIO) :只有Unix系统支持
与I/O multiplexing (select and poll)相比,它的优势是,免去了select的阻塞与轮询,当有活跃套接字时,由注册的handler处理。
asynchronous I/O (the POSIX aio_functions)
很少有*nix系统支持,windows的IOCP则是此模型
完全异步的I/O复用机制,因为纵观上面其它四种模型,至少都会在由kernel copy data to appliction时阻塞。而该模型是当copy完成后才通知application,可见是纯异步的。好像只有windows的完成端口是这个模型,效率也很出色。
服务器程序策略
服务器程序策略主要指的是网络编程时的开发策略,记得原来毕业找工作面试后端开发职位的时候这是一定会被问到。在讨论这个问题之前,必须要说的就是著名的C10K问题。
C10K是current 10k connection的简写,描述的是服务端如何处理同时到来的上万个client连接的问题,简而言之就是高并发的问题。为了解决这个问题,有以下几种经典的服务端策略:
这个策略是指服务器为每一个到来的连接都分配一个新的线程/进程,使用阻塞式的I/O来处理。Java和Apache都是这种策略,这种策略简单并且,能实现比较复杂的交互。然后系统分配进程/线程是需要资源的,而且因为使用了阻塞式的I/O,当有大量连接到来时候系统资源会是性能瓶颈。
这个思路是最直观,最容易想到的,以至于造成最大的误解:
当我们听说Node.js是单线程模型的时候就认定了他和开发服务器程序是无缘的。
第一种策略在高并发时会创建过多的线程,然而这些线程大部分时间都是在block等待数据。这种策略只用一个线程来监听多个socket连接,如果有数据到来就处理,读写完成后再次进入block监听。这种策略使用select/poll/epoll这样的多路复用I/O来实现。
同样是只用一个线程来处理所有的客户端连接,使用非阻塞I/O和事件机制来通知。Node.js就是使用了这种策略,这种策略实现简单,方便移植。缺点是只有一个线程所以不能充分利用CPU的多核性能,这也是Node.js用来开发服务器程序被吐槽最多的地方。
这是对第二种策略的改进型,分配多个线程,每个线程负责一定量的连接请求,使用非阻塞的I/O和事件机制。
与第三种策略相比,这种策略使用异步的I/O,由内核来完成数据的准备和copy。这种策略在支持异步I/O的操作系统上效率很高,用户无需任何操作就能完成数据的收发,这一切都由内核默默的完成。
同样的用上面那个小邮局取快递的例子再来说明下以上这四种网络编程策略:
每个线程同步阻塞处理一个连接:每个码农都在小邮局等自己的快递到来,快递到了之后自己把包裹取走;这种做法比较低效,大家都在等快递,都没时间搬砖了;
一个线程处理所有连接:让码农小明一个人在小邮局等所有的快递,只要有包裹来了就帮忙取走送到相应人座位上;这种方式只需要小明一个人,但是如果小明送包裹的速度不够快就会导致新来的包裹积压,不能及时送达;(这种需要很长时间才能送达的包裹就是CPU密集型的task)
多个线程,每个线程负责一组连接:这种方式就是多找一个小明一起来完成,是第二种方式的改进型;
多个线程,每个线程使用异步I/O负责一组连接:这种方式不用小明了,而是让小邮局的工作人员来做之前小明的工作,这样每个码农都能安心搬砖了,最大化了工作效率;
epoll也是IO复用,比传统的Select/poll有很大的优化,Linux2.6开始都使用了epoll,而且并不是信号驱动的,只是可以被中断信号中断。IO复用是调用方被动接受内核的通知,需要阻塞在Select/poll/epoll函数上,并不是单个IO上,就像你调用一个函数等待它返回一样,它返回的时候就是知道内核有数据到来的时候,它会通知你有哪些你注册过的IO上有数据了,但是数据存在内核中,需要你自己去从内核拷贝数据到用户空间(同步方式),这点也是同步和异步的区别,异步方式的话,当你收到通知的时候数据已经在用户空间了;信号驱动方式是基于系统的信号机制,你要先注册一个信号处理函数(当你收到系统信号时进行何种反应),处理函数会在另一个线程中执行,所以并不会阻塞当前的进程,当内核知道有数据到来,就向对应的进程发送信号,通知他数据来了,快来读,进程收到信号,启动一个线程执行对应的信号处理函数。
http://www.programgo.com/article/63742464168/