黑基网 首页 服务器 Windows Server 查看内容

Io不再神秘

2014-10-11 14:47| 投稿: winserver

摘要: 各种各样的I/O根据操作的阻塞或非阻塞类型,以及IO的准备就绪、完成事件通知的同步和异步类型,一共有四种不同方式的IO。同步阻塞IO在许多web server上,典型的一个连接一个thread的基础,...
各种各样的I/O根据操作的阻塞或非阻塞类型,以及IO的准备就绪、完成事件通知的同步和异步类型,一共有四种不同方式的IO。同步阻塞IO在许多web server上,典型的一个连接一个thread的基础,这种类型是IO操作阻塞着应用程序直到完成。当阻塞式的read方法或write方法被调用时,将有一次上下文切换至kernel中,IO操作会发生,数据会被复制进kernel的buffer中。然后,kernel buffer会把数据转给用户空间里的应用程序级别的buffer,并且应用程序的thread会被标识为runnable的,此时应用程序会解锁可以从用户空间的buffer中读取数据。一个连接一个thread的模型想尝试减少强制一个连接给一个thread的阻塞影响,需要掌握剩下的并发连接不再被IO操作在同一个连接上阻塞。当连接些都很短且数据延迟都不是很坏的时候这工作得很好。尽管如此,一旦连接变长且数据连接高延迟,可能性就是,线程些被连接长时间抓住不放,因为新连接的饥饿。如果使用定长的线程池,直到阻塞的线程在阻塞状态中不能被重用以服务新的连接,如果每个新的连接用一个新的线程服务,或者会导致大量的线程会在系统中被产生,这会演变为漂亮的资源争抢,为了完成高并发的负载,而高上下切换消费。1 2 3 4 5 ServerSocket server = new ServerSocket(port); while(true) { Socket connection = server.accept(); spawn-Thread-and-process(connection); }简单的一连接一线程server同步非阻塞IO这个模型下,设备(网卡)或者连接被设置为非阻塞的,read()和write()操作将不会被阻塞。通常意味着,如果操作不能立即得到结论,将会返回,带一个error code以指出操作会阻塞(POSIX标准是EWOULDBLOCK)或者是设备临时不可用(POSIX标准是EAGAIN)。由应用程序去检测,直到设备准备好了并且所有数据被读到。尽量如此,这也不是非常高效,因为每次调用都会激起一次上下文切换给kernel,并且不会考虑数据有没被读到。带就绪事件的异步非阻塞IO前面的模型的问题在于,应用程序不得不检测,会忙于等待任务完成。当设备准备好被读写时,有更好的办法通知应用程序吗?这的确就是本模型所提供的好处。使用特殊的系统调用(因平台而变-linux下使用select()/poll()/epoll(),BSD使用kqueue(),Solaris使用/dev/poll),应用程序注册感兴趣的点收集IO就绪的信息,从特定的设备(在Linux下使用文件描述符,所有的sockets都被抽象使用了文件描述符),特定的IO操作(读或写)。然后,这个系统调用被调用,至少其中一个被注册的文件描述符变成ready之前,这调用会被阻塞。一旦这个文件描述符准备好做IO操作了,就会被取来当作系统调用的返回,然后系统调用就可以在应用程序的loop中被顺序地调用。准备好的连接处理逻辑经常包括一个用户提供的事件handler,此handler会一起发起非阻塞的read()/write()调用,目的是从设备取数据给kernel,最终给用户空间的buffer,这会激起上下文切换到kernel。无论如何,通常没有绝对保证,有可能会发生,设备上预期的由操作系统提供的IO,只是一个指示,设备有可能准备好感兴趣的IO操作了,但read()或write()却不行。尽管如此,与标准情况相比这应该算异常了。所以,总结的办法就是,在异步流中获取就绪事件,注册一些事件处理器,当有类似的事件通知被触发的时候抓住他们。正如你所见,所有的事情都可以在一个单独的线程中完成,即便从多个不同连接过来的多路传输,主要因为select()(这里我选择了典型的系统调用),已经是可以同一时间返回多个sockets准备就绪的类型。同一时间在多个sockets上返回就绪,这只是一部分好处。这种类型就是经常没提供的非阻塞IO模型。Java已经抽象出来平台特殊性系统调用的不同,实现了NIO API。Socket 文件描述符被用Channels和Selector抽象,封装到selection系统调用中。应用程序感兴趣的收集就绪事件,注册到Channel(通常在ServerSocketChannel上accept()就得到一个SocketChannel),注册的内容是Selector,会得到SelectionKey,这个SelectionKey就是作为一个handle,这个handle的作用是hold住Channel和注册信息。然后阻塞的select()调用被设置在Selector,它会返回一系列的SelectionKey,然后一个接一个地被程序所指定的事件处理器所处理。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 elector selector = Selector.open(); channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ); while(true) { int readyChannels = selector.select(); if(readyChannels == 0) continue; Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove(); } }简单的非阻塞server带完成事件的异步非阻塞IO就绪事件只能做到通知你设备\socket准备好做事情的程度。应用程序依然不得不做脏活,为了从设备/socket中读数据(更准确地说是通过系统调用指示操作系统),通过设备的各种思路将数据扔到用户空间的buffer。把任务代理给操作系统在后台运行,一旦完成了让它再通知你,包括从设备到kernel的buffer再最终到应用程序级别的buffer传送所有的数据,这样岂不是很爽?这就是经常被提到的异步IO模型背后的基础想法。所以需要操作系统层支持AIO操作。在Linux下从2.6开始在aio POSIX API中被支持,Windows下用I/O Completion Ports支持。JAVA NIO2在AsynchronousChannel API中一点点支持此模型。操作系统支持为了支持就绪和完成事件通知,不同的操作系统提供了各种各样的系统调用。就绪事件 select()和poll()可以在Linux类的系统中使用。尽管如此,更新的epoll()变种更好,因为它比select()和poll()更有效率。当监控的文件描述符增长时,选择时间在线性增长,这一点上导致了select()不行。在复写文件描述符数组这事上已经臭名昭著。所以每次一被调用,描述符数组就需要从一个单独的拷贝上重新构建。无论如何这都不是一个优雅的解决方案。epoll()变体可以按两种办法被配置,边沿触发和层级触发。在边沿触发情况下,只有在相关的描述符上事件被检测到才会发出通知。说了在一个事件触发通知期间,你的应用程序触发器只会读一半kernel的输入buffer。现在在这个描述符上不会得到通知,甚至到下一个时间周期,除非设备准备好发更多的数据,否则有一点点数据可读的时候也不会有通知,有足够的数据的时候会导致一次文件描述符的事件。层级触发用另一方式配置,每次数据可读了都会触发通知。相比的系统调用还有BSD口味的kqueue,Solaris由于版本不同有/dev/poll或者”Event Completion”。Windows下等价的是“I/O Completion Ports”。至少在Linux下AIO模型的情况却大不同。Linux中aio的支持看上去埋头在一些意见困扰中,实际地在kernel层面使用就绪事件,同时在应用程序层面提供异步完成事件的抽象。尽管如此,Windows看上去通过“I/O Completion Ports”支持这个得了第一名。IO模式101在软件开发中到处是设计模式。I/O不一样。只有两种I/O模式,NIO和AIO,下面进行介绍。Reactor模式有许多组件使用这个模式来实现。我会解释一遍先,后面好看懂代码。Reactor启动器:这是会初始化非阻塞服务器的组件,主要是配置和初始化分配器(dispatcher)。首先,它会bind出服务器的socket,并且通过分接器(demultiplexer)注册,分接器作用是客户端连接接收就绪事件。然后就绪事件(读写接收等)的每种类型的事件处理器实现会被注册到分配器(dispatcher)。下一次分配器事件loop过程会被调起来,以处理事件通知。dispatcher:为注册、删除定义接口,分发事件处理器起作用,作用是响应连接事件,包括连接被接受、数据输入输出、一组连接上的超时事件。为了服务一个客户端连接,相关的事件处理器(比如接受事件处理器)会被注册给被接受的客户端通道(在client socket其下包装),注册内容是分接器(demultiplexer),就绪事件类的都被会注册,以监听此特定的channel。然后,分配器线程会调出阻塞的就绪选择操作,这些操作在demultiplexer之上,主要为剩下的注册通道。一旦一个或多个被注册的通道准备好IO,分配器会服务给相关的每个准备好的通道一对一的用注册的事件处理器返回“Handle”。很重要的是,这些事件处理器不会hold住分配器线程,但是会延迟分配器服务其他准备好的连接。因为常见的在事件处理器里的逻辑,包括传送数据从/去准备好的连接,这些连接会阻塞,一直到所有的数据在用户空间和内核数据缓存中被送完,一般情况下,这些处理器跑在一个线程池的不同的线程里。Handle:当一个channel被注册了分接器(demultiplexer)就会返回一个handle,handle概括了连接通道和就绪信息。靠分接器就绪选择操作,一系列的准备好的Handle会被返回。Java NIO里对等的叫SelectionKey。Demultiplexer:(分接器:54chen专门瞎翻)等待在一个或多个注册的连接通道里的就绪事件。Java NIO里叫Selector。Event Handler:指接口具有的hook方法,以分配连接事件。这些方法需要被应用程序指定的事件处理器所实现。Concrete Event Handler:(具体的事件处理器)包括从连接中读写数据的逻辑,并且要做一些必须的过程,或者初始化客户端连接传过的接收协议,这些协议来自通过的Handle。事件处理器典型地跑在一个线程池的单独的线程中,下面的图片中显示了这一过程。一个简单的echo server实现,下面的例子显示了这种模式(没有事件处理器线程池)。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 小编推荐:欲学习电脑技术、系统维护、网络管理、编程开发和安全攻防等高端IT技术,请 点击这里 注册黑基账号,公开课频道价值万元IT培训教程免费学,让您少走弯路、事半功倍,好工作升职加薪!



免责声明:本文由投稿者转载自互联网,版权归原作者所有,文中所述不代表本站观点,若有侵权或转载等不当之处请联系我们处理,让我们一起为维护良好的互联网秩序而努力!联系方式见网站首页右下角。


鲜花

握手

雷人

路过

鸡蛋

相关阅读

最新评论


新出炉

返回顶部