JVM的多路复用器实现原理

  • Linux 2.5以前:select/poll
  • Linux 2.6以后: epoll
  • Windows: Winsock的select模型(感谢评论指正,仅Java NIO.2使用了Windows IOCP,由于Netty没有采用NIO.2此处不展开)
  • Free BSD, OS X: kqueue

下面仅讲解Linux的多路复用。

Linux中的IO

Linux的IO将所有外部设备都看作文件来操作,与外部设备的操作都可以看做文件操作,其读写都使用内核提供的系统调用,内核会返回一个文件描述符(fd, file descriptor),例如socket读写使用socketfd。描述符是一个索引,指向内核中一个结构体,应用程序对文件的读写通过描述符完成。

一个基本的IO,涉及两个系统对象:调用这个IO进程的对象,系统内核,read操作发生时流程如下:

  1. 通过read系统调用向内核发起读请求。
  2. 内核向硬件发送读指令,并等待读就绪。
  3. 内核把将要读取的数据复制到描述符所指向的内核缓存区中。
  4. 将数据从内核缓存区拷贝到用户进程空间中。

Linux I/O模型简介

  1. 阻塞I/O模型:最常用,所有文件操作都是阻塞的。
  2. 非阻塞I/O模型:缓冲区无数据则返回,一般采用轮询的方式做状态检查。
  3. I/O复用模型:详细见下
  4. 信号驱动I/O:使用信号回调应用,内核通知用户何时开启一个I/O操作。
  5. 异步I/O:内核操作完成后进行通知,内核通知用户何时完成一个I/O操作。

Linux IO 多路复用

使用场景

  • 客户处理多个描述符(交互输入,网络套接口)
  • 客户处理多个套接口(少见)
  • TCP服务器既要处理监听套接口,又要处理已连接套接口。
  • 一个服务器既要处理TCP,又要处理UDP
  • 一个服务器处理多个服务/多个协议

与多进程/多线程对比

I/O多路复用系统开销小,系统不必创建进程/线程,也不需要维护这些进程/线程。

系统调用

目前支持I/O多路复用的系统调用包括select,pselect,poll,epoll,I/O多路复用即通过一种机制,一个进程可以监视多个描述符,一旦某个描述符准备就绪,就能够通知程序进行相应的读写操作。

select/poll

select目前在所有平台支持,select函数监视文件操作符(将fd加入fdset),循环遍历fdset内的fd获取是否有资源的信息,若遍历完所有fdset内的fd后无资源可用,则select让该进程睡眠,直到有资源可用或超时则唤醒select进程,之后select继续循环遍历,找到就绪的fd后返回。select单个进程打开的fd有一定限制,由FD_SETSIZE设置,默认为1024(32位)和2048(64位)。

poll与select的主要区别是不使用fdset,而是使用pollfd结构(本质链表结构),因而没有fd数目限制。

poll和select共有的问题:

  • 每次select/poll找到就绪的fd,都需要把fdset/pollfd进行内存复制。
  • select/poll,都要在内核中遍历所有传递来的fd来寻找就绪的fd,随着监视的fd数量增加,效率也会下降。

epoll

Linux 2.6内核中提出了epoll,epoll包括epoll_create,epoll_ctl,epoll_wait三个函数分别负责创建epoll,注册监听的事件和等待事件产生。

  • epoll每次注册新的事件到epoll中时,都会把所有fd拷贝进内核,而不是在epoll_wait时重复拷贝,保证每个fd在整个过程中仅拷贝一次。此外,epoll将内核与用户进程空间mmap到同一块内存,将fd消息存于该内存避免了不必要的拷贝。
  • epoll使用事件的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就通过回调函数把就绪的fd加入一个就绪链表,唤醒epoll_wait进入睡眠的进程,epoll_wait通知消息给应用程序后再次睡眠。因此epoll不随着fd数目增加效率下降,只有活跃fd才会调用回调函数,效率与连接总数无关。
  • epoll没有最大并发连接的限制,1G内存约能监听10万个端口。

epoll有LT模式和ET模式:

  • LT模式:epoll_wait检测到fd并通知后,应用程序可以不立刻处理,下次调用epoll_wait,会再次通知;
  • ET模式:应用程序必须立刻处理,下次调用,不会再通知此事件。ET模式效率更高,epoll工作在ET模式下必须使用非阻塞套接字。

性能对比

  • 如果有大量的idle-connection或dead-connection,epoll效率比select/poll高很多。
  • 连接少连接十分活跃的情况下,select/poll的性能可能比epoll好。

Java的IO模式

  1. BIO:即传统Socket编程,线程数:客户端访问数为1:1,由于线程数膨胀后系统性能会急剧下降,导致了BIO的低效。
  2. 伪异步I/O:为了解决一个链路一个线程的问题,引入线程池处理多个客户端接入请求,可以灵活调配线程资源,可以限制线程数量防止膨胀,但底层仍是阻塞模型,高客户端访问时,会有通信阻塞的问题。
  3. NIO:Java NIO的核心为Channels, Buffers, Selectors。Channel有点像流,数据可以从Channel读到Buffer中,也可以从Buffer写到Channel内。而Selector则被用于多路复用,Java NIO可以把Channel注册到Selector上,之后,Selector会获取进入就绪状态的Channel(Selector进行循环的select/poll/epoll/IOCP操作),并进行后续操作。Selector是NIO实现的关键。Java NIO编程较为复杂。
  4. AIO:NIO.2引入的异步通道概念,不需要多路复用器对注册的通道轮询即可异步读写,简化了NIO的编程。(但是Netty作者称AIO的性能并不比NIO和epoll好)

Netty

使用Netty而非直接使用Java NIO出于以下原因:

  1. Java NIO的API过于繁杂。
  2. Java NIO开发需要了解Reactor模型,Java多线程等。
  3. Java NIO低可靠性。
  4. Java NIO有很多臭名昭著的BUG,如NIO的epoll空轮询bug

下面简单介绍下Netty的部分功能。

ByteBuf

Netty的ByteBuf依然是Byte数组缓冲区,提供对基础类型,byte[]数组,ByteBuffer,ByteBuf的读写,缓冲区自身的copy和slice,操作指针,字节序,构造实例等功能。相对于ByteBuffer,ByteBuf的读写采用两个指针而非flip方案,增加了可靠性,并提供了自动扩展方案。

ByteBuf的内存池实现比较复杂,但是否使用内存池,有较大的性能差异。随着JVM和JIT的发展,对象的分配和回收是个轻量级的工作,但是对于缓冲区Buffer,特别是堆外直接内存的分配和回收则仍很耗时。Netty提供了基于内存池的缓冲区重用机制,带来了性能提高。UnpooledByteBufAllocator在Netty4仍然是默认的allocator,但在大多情况下,PooledByteBufAllocator将带来更高性能。更改默认方式仅需在初始化时加以设置:

客户端

 b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)

服务端

.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.config().setAllocator(PooledByteBufAllocator.DEFAULT);

Channel和Unsafe

Netty的Channel和NIO的Channel类似,但有自己的子类和实现。Unsafe则封装了Netty不希望用户调用的API,作为Channel的辅助类。

Channel包括而不限于网络的读,写,客户端发起连接,主动关闭连接,链路关闭,获取双方通信地址等功能。Channel也包括了Netty框架的相关功能,如获取该Channel的EventLoop,获取缓冲区分配器ByteBufAllocator和Pipeline等。Channel封装了Java NIO不统一的SocketChannel和ServerSockerChannel,其接口定义大而全。

Unsafe是Channel的辅助接口,实际的I/O读写操作都是由Unsafe完成的。包括register,bind,disconnect,close,write,flush几个接口,可以看到它更接近于原本的Java NIO Channel。

ChannelPipeline和ChannelHandler

Netty的pipeline和handler机制类似于Servlet和Filter,为了方便拦截和业务逻辑定制。Netty将Channel的管道抽象为ChannelPipeline,让消息在其中流动,ChannelPipeline持有消息拦截器ChannelHandler列表,可以通过增加和删除handler来改变业务逻辑,而不是对已有的handler进行修改。

ChannelHandler的种类繁多,且用户可以自定义,自定义时,通常只需要继承ChannelHandlerAdapter并重写为了实现业务逻辑的必要方法即可。

此外,ChannelPipeline支持运行时动态添加或删除ChannelHandler,某些场景下这个特性很实用。

ChannelPipeline是线程安全的,但ChannelHandler不是线程安全的,需要用户自己进行保障。

EventLoop和EventLoopGroup

Netty的线程模型得以无锁化依赖于其NioEventLoop。因此,此处详细展开。

Netty的线程模型

  • Reactor模型:所有I/O都在NIO线程完成,NIO线程作为服务端,接收所有客户端TCP连接,并处理链路。
  • Reactor多线程模型:由专门的一个Acceptor线程监听服务端,接收客户端的TCP请求,并调度一个subReactor线程池,该线程池维护多个处理线程,一个NIO线程可以处理N条链路。
  • 主从Reactor多线程模型:在上述Reactor多线程模型基础上,服务端接收客户端TCP请求的不再是一个NIO线程,而是一个独立的NIO线程池,Acceptor线程池仅用于客户端的登录和认证,链路建立成功就交给subReactor线程池做后续操作。

Netty线程池:服务端启动时,创建bossGroup, workerGroup两个NioEventLoopGroup,实际上是两个Reactor线程池,一个用于接收客户端TCP请求,一个用于处理I/O读写或执行业务。

  • 接收线程池(bossGroup)职责:接收客户端TCP连接,初始化Channel参数,将链路状态变更通知给ChannelPipeline。
  • I/O处理线程池(workerGroup)职责:异步读取通信对端数据,发送读事件给ChannelPipeline;异步发送消息到通信对端,调用ChannelPipeline的消息发送接口;执行业务或系统调用/定时任务等工作。

通过调整bossGroup和workerGroup的线程个数,group()函数参数数量,是否共享线程池等,Netty的Reactor模型可以在单线程,多线程,主从多线程等模式中切换。

NioEventLoop

Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的fireChannelRead方法,只要用户不切换线程,一直都由NioEventLoop调用用户的Handler,期间不切换线程,而是串行化运行handler,避免了多线程操作的锁的竞争,达到性能最优。

NioEventLoop不纯粹是一个IO线程,它既可以处理系统Task又可以处理定时任务。

Future和Promise

Future起源于JDK的Future,Netty的Future命名为ChannelFuture,与Channel操作有关。Netty中所有操作都是异步的,因此,获取异步操作结果,就要交给ChannelFuture。ChannelFuture有completed何uncompleted两种状态,创建后处于uncompleted状态,一旦I/O操作完成,则被设置成completed状态,此时可能操作失败,操作成功或操作被取消。和JDK的Future类似,ChannelFuture有很多方便的API,包括获取操作结果,添加事件监听器,取消I/O操作,同步等待等。

Promise是可写的Future,用于设置I/O额结果。Netty发起I/O操作时,会创建一个新的Promise对象。

参考文献

聊聊IO多路复用之select、poll、epoll详解

关于同步,异步,阻塞,非阻塞,IOCP/epoll,select/poll,AIO ,NIO ,BIO的总结

【Java】从BIO、NIO到Linux下的IO多路复用

OSX/iOS中多路I/O复用总结

java nio及操作系统底层原理

Select函数实现原理分析

Netty4底层用对象池和不用对象池实践优化

设置Netty接收Buff为堆内存模式

关于java nio在windows下实现

相关阅读:NIO.2

NIO.2 uses IOCP

在 Java 7 中体会 NIO.2 异步执行的快乐

Java IO & NIO & NIO2

5种调优Java NIO和NIO.2的方式

Java多线程:Linux多路复用,Java NIO与Netty简述的更多相关文章

  1. Java多线程编程(1)--Java中的线程

    一.程序.进程和线程   程序是一组指令的有序集合,也可以将其通俗地理解为若干行代码.它本身没有任何运行的含义,它只是一个静态的实体,它可能只是一个单纯的文本文件,也有可能是经过编译之后生成的可执行文 ...

  2. Java多线程编程核心技术---Java多线程技能

    基本概念 进程是操作系统结构的基础,是一次程序的执行,是一个程序及其数据结构在处理机上顺序执行时所发生的活动,是程序在一个数据集合上运行的过程,是系统进行资源分配和调度的独立单位.线程可以理解成是在进 ...

  3. 【java多线程系列】java内存模型与指令重排序

    在多线程编程中,需要处理两个最核心的问题,线程之间如何通信及线程之间如何同步,线程之间通信指的是线程之间通过何种机制交换信息,同步指的是如何控制不同线程之间操作发生的相对顺序.很多读者可能会说这还不简 ...

  4. java 多线程5: java 终止线程及中断机制 (stop()、interrupt() 、interrupted()、isInterrupted())

    JAVA中有3种方式可以终止正在运行的线程 ①线程正常退出,即run()方法执行完毕了 ②使用Thread类中的stop()方法强行终止线程.但stop()方法已经过期了,不推荐使用 ③使用中断机制i ...

  5. java多线程基础(二)--java多线程的基本使用

    java多线程的基本使用 在java中使用多线程,是通过继承Thread这个类或者实现Runnable这个接口或者实现Callable接口来完成多线程的. 下面是很简单的例子代码: package c ...

  6. (1)Java多线程编程核心——Java多线程技能

    1.为什么要使用多线程?多线程的优点? 提高CPU的利用率 2.什么是多线程? 3.Java实现多线程编程的两种方式? a.继承Thread类 public class MyThread01 exte ...

  7. Java多线程系列一——Java实现线程方法

    Java实现线程的两种方法 继承Thread类 实现Runnable接口 它们之间的区别如下: 1)Java的类为单继承,但可以实现多个接口,因此Runnable可能在某些场景比Thread更适用2) ...

  8. (原创)Java多线程作业题报java.lang.IllegalMonitorStateException解决

    作业: 有一个水池,水池容量500L,一边为进水口,一边为出水口,要求进水放水不能同时进行,水池一旦满了不能继续注水,一旦空了,不能继续放水,进水速度5L/s,放水速度2L/s. 这是我学多线程时做的 ...

  9. 【java多线程系列】java中的volatile的内存语义

    在java的多线程编程中,synchronized和volatile都扮演着重要的 角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性,可见性指的是当一 ...

随机推荐

  1. openwrt 添加 802.1x客户端njit

    1.修改feed的配置文件 feeds.conf.default 添加下面两句: src-svn njit https://github.com/liuqun/openwrt-clients/trun ...

  2. git命令行提交并且同步到远程代码库

    远程代码库以github为例 1.打开 git bash 2.进入项目目录 cd /e/myGitProjects/test 3.提交到本地git仓库 git add -Agit commit -m ...

  3. 《java程序设计》结对编程-四则运算(第一周-阶段总结)

    一.需求分析(描述自己对需求的理解,以及后续扩展的可能性) 实现一个命令行程序,要求: - 自动生成小学四则运算题目(加,减,乘,除) - 支持整数 - 支持多运算符(比如生成包含100个运算符的题目 ...

  4. Scala集合笔记

    Scala的集合框架类比Java提供了更多的一些方便的api,使得使用scala编程时代码变得非常精简,尤其是在Spark中,很多功能都是由scala的这些api构成的,所以,了解这些方法的使用,将更 ...

  5. LeetCode(60): 第k个排列

    Medium! 题目描述: 给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列. 按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下: "123" ...

  6. cf803c 数论

    细节很多的题 #include<bits/stdc++.h> using namespace std; #define ll long long int main(){ ll n,k,tm ...

  7. python 全栈开发,Day88(csrf_exempt,ES6 快速入门,Vue)

    BBS项目内容回顾 1. 登陆页面 1. 验证码 1. PIL(Pillow) 2. io 2. ORM 1. 增删改查 3. AJAX $.ajax({ url: '', type: '', dat ...

  8. python 全栈开发,Day87(ajax登录示例,CSRF跨站请求伪造,Django的中间件,自定义分页)

    一.ajax登录示例 新建项目login_ajax 修改urls.py,增加路径 from app01 import views urlpatterns = [ path('admin/', admi ...

  9. hdu 2066 多起点 多终点

    多起点 多终点 无向图 结点的个数要自己求 Sample Input6 2 3 //边数 起点数 终点数1 3 5 //u v w1 4 72 8 123 8 44 9 129 10 21 2 //起 ...

  10. Hibernate之关联关系映射(一对多和多对一映射,多对多映射)

    ~~~接着之前的Hibernate框架接着学习(上篇面试过后发现真的需要学习一下框架了,不然又被忽悠让去培训.)~~~ 1:Hibernate的关联映射,存在一对多和多对一映射,多对多映射: 1.1: ...