Netty指南(1)--- I/O模型介绍

前言

本文我们将会介绍JavaI/O进化过程、Linux系统中网络编程I/O的五种模型,以及I/O多路复用技术。

JavaI/O发展史

众所周知Netty是由Java开发,那我们先简单的聊聊Java。
Java是由Sun Microsystems公司1995年发布的一款高级编程语言,由于其拥有 一次编写、到处运行 的特性和丰富且强大的第三方类库支持,自发布以来应用极其广泛,但是Java早期的功能并不完善,其中最令人恼火就是基于同步I/O的Socket通信类库,因为对于操作系统而言,底层是支持异步I/O通信的,只不过很长一段时间Java并没有提供异步I/O通信的类库,JDK1.0 ~ JDK1.3期间Java的IO类库都非常原始,很多Unix网络编程中的接口在Java的I/O类库中都没有体现,例如Pipe、Channel、Buffer、Selector等。

Java早期I/O(<1.4)的一些明显问题(包括但不限于)

  • 没有C/C++中Channel的概念,只有输入和输出流
  • 同步阻塞式通信I/O,当并发量变大时,经常会导致线程被长时间阻塞且占用资源大
  • 没有数据缓冲区,I/O性能相对较差
    直到2002年随着JDK1.4的发布Java才第一次支持非阻塞I/O,这个类库的提供为JDK的通信模型带来了巨大的变化。

    JDK1.4新增主要的类/接口如下

  • 异步IO操作的缓冲区ByteBuffer
  • 进行异步IO操作的管道Pipe
  • 进行IO操作的Channel,包括ServerSocketChannel和SocketChannel
  • 文件通道FileChannel
  • 多种字符集编解码能力
  • 实现非阻塞IO操作的多路复用器Selector
  • 基于Perl实现的正则表达式类库等……
    新的NIO类库的提供极大地促进了Java异步非阻塞编程的发展和应用,但仍有不完善的地方。

    早期NIO类库主要问题如下

  • 没有统一的文件属性,例如读写权限
  • API能力比较弱,例如目录的联级创建和递归遍历,往往需要自己实现
  • 底层存储系统的一些高级API无法使用
  • 所有文件操作都是同步阻塞调用的,不支持异步文件读写操作
    2011年,JDK1.7正式发布,最大的亮点是NIO类库的升级,NIO2.0(AIO诞生)。

    主要提供如下改进

  • 提供能够批量获取文件属性的API,这些API具有平台无惯性,不与特定的文件系统耦合
  • 提供AIO功能,支持基于文件和网络套接字的异步操作

Linux网络I/O模型

Linux内核对外部设备都看作一个文件来操作,一个文件的读写操作会调用内核命令,返回一个文件描述符-fd,对于socket的读写也会有相应的描述符-socketfd,它指向内核中的一个结构体。
根据Unix网络编程对I/O模型的分类,共分为5种I/O模型,分别如下:

同步阻塞I/O模型

最常用的I/O模型,所有文件操作都是阻塞的,以套接字接口为例,在用户进程空间调用recvfrom(用于套接口上接收数据,并捕获数据发送源的地址),其系统调用直到数据包到达且数据包被复制到用户进程的缓冲区或发生错误时才返回,在此期间会一直处于阻塞状态,因此被称为阻塞I/O模型。

同步非阻塞I/O模型

继续以上面的套接字接口为例,在用户进程空间中调用recvfrom,从应用层到内核的时候,有数据直接返回,没数据会返回一个EWOULDBLOCK错误,一般都会对非阻塞I/O进行状态轮训,看内核中有没有数据过来。

I/O复用模型

Linux提供了select、poll,进程通过一个或多个文件描述符-fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮我们侦测多个fd是否处于就绪。
select和poll的实现方式类似,不同之处在于select有fd的限制,poll采用pollfd链式结构代替select中fd_set使其没有fd数量的限制,但是其内部实现都是顺序扫描所有fd的状态,并且每次调用select/poll都需要把fd集合从用户空间copy到内核空间,因此当连接数变大处理速度会呈线性下降,因此它的使用受限。
Linux还提供了一个epoll系统调用,epoll为每个fd指定了一个回调函数,当数据准备好后,就绪的fd会主动加入到一个就绪队列中,唤醒就绪fd的等待者,调用回调函数。
epoll使用基于事件驱动方式代替select/poll中的顺序扫描,因此性能提高很多。

信号驱动I/O模型

信号驱动IO模型的特点是无需等待数据,在数据等待阶段是非阻塞的,内核当数据准备就绪时会为该进程生成一个SIGIO信号,应用程序只需要绑定SIGIO信号的处理函数就可以了,通过SIGIO信号回调通知应用程序来读取数据,并通知”属主”进程执行SIGIO信号处理函数处理数据。
信号驱动I/O模型因为其无需等待数据就绪的特性,比select和poll的性能高,但是缺点是致命的,Linux中信号队列的大小是有限制的,一旦队列溢出,进程将终止导致无法读取数据。

异步I/O模型

告知内核启动某个操作,并让内核在整个操作后(包括将数据从内核复制到用户缓冲区)通知我们。
这种模型与信号驱动模型主要区别是 信号驱动IO由内核通知我们何时开始一个I/O操作,异步I/O模型由内核通知我们IO操作何时已经操作完成。

IO多路复用技术

在IO编程过程中,当需要同时处理多个客户端接入请求时可以利用多线程或I/O多路复用技术进行处理。
I/O多路复用技术通过把多个IO的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。
与传统多线程/多进程相比,IO多路复用的优势在于系统开销小,系统不需要创建新的额外进程/线程,也不需要维护这些进程/线程的运行,降低了系统维护的工作量同时也节省了系统资源。
目前支持的I/O多路复用系统调用有select、pselect、poll、epoll,在Linux网络编程过程中,很长一段时间都使用select做轮训和网络事件通知,由于select的固有缺陷,导致它的应用受到了很大的限制,最终Linux在新的内核版本中选择了epoll。

epoll与select相比

  • 支持一个进程打开的socket描述符-fd不受限制(仅受限于操作系统的最大文件句柄数)

    select最大的缺陷是单个进程所能打开的fd有限制,它由FD_SETSIZE设置,缺省值是1024,对于那些需要支持成千上万个TCP连接的服务器显然太少了,可以选择修改这个宏重新编译内核,不过会带来网络效率下降的问题。也可以使用传统的多进程方案解决这个问题,虽然在Linux中创建进程的代价比较小,但随着连接数的增加资源占用还是会线性增长,所以这种方式并不能解决根本问题。
    epoll并没有这个限制,它所支持的fd上限是系统可创建的最大文件句柄数,要远远大于select中的FD_SETSIZE,比如一台1GB的机器上可以创建10W个句柄左右,具体的只可以通过Linux系统命令 cat /proc/sys/fs/file-max 查看系统最大文件句柄数,通常这个值与系统的内存关系比较大,可人为根据实际机器配置情况修改这个值。

  • IO效率不会随着fd的数量增加而线性下降

    传统select/poll的一个致命缺点是当拥有一个很大的socket集合时,由于网络延时或链路空闲,任一时刻只有少部分的socket是活跃的,但是select/poll每次调用都会线性扫描全部的集合,导致效率成线性下降。
    epoll中不存在这个问题,它只会对活跃的socket进行操作,这是因为在内核实现中epoll是根据每个fd的callback函数实现的,只有活跃的socket才会主动调用callback函数,其他idle状态的socket则不会,这也是为什么有人称epoll为”异步IO”。
    针对epoll和select性能对比的benchmark测试表明:如果所有的socket都处于活跃状态,在这种情况下epoll的效率并不比select/poll高,反而有些许下降,但是正常的网络环境参差不齐,socket基本不会全部处于活跃状态,在这种情况下epoll的性能远高于select/poll。

  • 使用mmap加速内核与用户控件的消息传递

    无论是select、poll还是epoll都需要内核把fd消息通知给用户空间,如何避免不必要的内存复制是非常重要的。
    epoll是通过内核和用户控件mmap同一块内存来实现的,避免了不必要的内存复制。

  • 补充说明

    克服select/poll缺点的方法不只有epoll,epoll只是Linux的实现方案,在freeBSD(另外一种操作系统)中的kqueue同样可以克服select/poll的缺点,但是使用难度较高,而且我们的应用程序目前绝大多数都是部署在Linux中的。

Choice wechat
关注公众号,获取文章更新通知。
-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!