1.I/O要解决什么问题?
I/O要解决什么问题?
- I/O:在计算机内存与外部设备之间拷贝数据的过程。
- 程序通过CPU向外部设备发出读指令,数据从外部设备拷贝至内存需要一段时间,这段时间CPU就没事情做了,程序就会两种选择:
1)让出CPU资源,让其干其他事情
2)继续让CPU不停地查询数据是否拷贝完成
到底采取何种选择就是I/O模型需要解决的事情了。
以网络数据读取为例来分析,会涉及两个对象,一个是调用这个I/O操作的用户线程,另一个是操作系统内核。一个进程的地址空间分为用户空间和内核空间,基于安全上的考虑,用户程序只能访问用户空间,内核程序可以访问整个进程空间,只有内核可以直接访问各种硬件资源,比如磁盘和网卡。

1.1 文件数据读取

过程:
- in.read(buf)执行时,JVM向kernel发起system call read()
- 操作系统发生上下文切换,由用户态(User mode)切换到内核态(Kernel mode),把数据读取到Kernel space buffer
- Kernel把数据从Kernel space复制到User space,同时由内核态转为用户态。
- JVM继续执行out.write(buf)这行代码
- 再次发生上下文切换,把数据复制到Kernel space buffer中。kernel把数据写入文件。
1.2 网络数据读取
当用户线程发起 I/O 调用后,网络数据读取操作会经历两个步骤:
- 数据准备阶段: 用户线程等待内核将数据从网卡拷贝到内核空间。
- 数据拷贝阶段: 内核将数据从内核空间拷贝到用户空间(应用进程的缓冲区)。

2.I/O模型种类
在Linux中一共有5种I/O模型,分别如下:
- blocking I/O
- nonblocking I/O
- I/O multiplexing (select 、poll、 epoll)
- signal driven I/O (SIGIO)
- asynchronous I/O (the POSIX aio_functions)
2.1 blocking I/O
两阶段:
- 等待数据阶段:系统调用recvfrom阻塞等待内核将数据准备好
- 数据拷贝阶段: 用户发起的(同步)recvfrom等待内核将数据准备好后,就将数据复制到用户态缓冲区

blocking I/O发起system call recvfrom()时,进程将一直阻塞等待另一端Socket的数据到来。在这种I/O模型下,我们不得不为每一个Socket都分配一个线程,这会造成很大的资源浪费。
Blocking I/O优缺点都非常明显。优点是简单易用,对于本地I/O而言性能很高。缺点是处理网络I/O时,造成进程阻塞空等,浪费资源。
注: read() 和 recvfrom()的区别是,前者从文件系统读取数据,后者从socket接收数据。
2.2 nonblocking I/O
两阶段:
- 等待数据阶段:系统调用recvfrom不会阻塞等待,而是不停的去轮询内核。
- 数据拷贝阶段: 用户发起的(同步)recvfrom检查到内核将数据准备好后,就将数据复制到用户态缓冲区

相对于阻塞I/O在那傻傻的等待,非阻塞I/O隔一段时间就发起system call看数据是否就绪(ready)。
如果数据就绪,就从kernel space复制到user space,操作数据; 如果还没就绪,kernel会立即返回EWOULDBLOCK这个错误。
recvfrom有个参数叫flags,默认情况下阻塞。可以设置flag为非阻塞让kernel在数据未就绪时直接返回。详细见recvfrom。
进程发起I/O操作时,不会因为数据还没就绪而阻塞,这就是”非阻塞”的含义。
2.3 I/O multiplexing (select 、poll、 epoll)
两阶段:
- 等待数据阶段:系统调用select/poll/epoll会阻塞等待某个套接字就绪时。
- 数据拷贝阶段: 用户发起的(同步)recvfrom将就绪的数据复制到用户态缓冲区
上面介绍的I/O模型都是直接发起I/O操作,而I/O Multiplexing首先向kernel发起system call,传入file descriptor和感兴趣的事件(readable、writable等)让kernel监测,当其中一个或多个fd数据就绪,就会返回结果。程序再发起真正的I/O操作recvfrom读取数据。

在linux中,有3种system call可以让内核监测file descriptors,分别是select、poll、epoll。
select:
- 1)流程
step1.程序阻塞等待kernel返回
step2.kernel发现有fd就绪,返回数量
step3.程序轮询3个fd_set寻找就绪的fd
step4.发起真正的I/O操作(read、recvfrom等) - 2)优点:
几乎所有系统都支持select
一次让kernel监测多个fd - 3)缺点:
每个fd_set的大小,受限于FD_SETSIZE(默认的1024)
kernel返回后,需要轮询所有fd找出就绪的fd,随着fd数量增加,性能会逐渐下降
poll:
- poll不受限FD_SETSIZE的值。它优缺点跟select差不多。另外是只有linux支持poll。
epoll:
- select和poll都是等待内核返回后轮询集合寻找就绪的fd,有没有一种机制,当某个fd就绪,kernel直接返回就绪的fd呢?这就是epoll。
- epoll是一种I/O事件通知机制,linux kernel 2.5.44开始提供这个功能,用于取代select和poll。epoll内部结构用红黑树实现,用于监听程序注册的fd。
- epoll是一种性能很高的同步I/O方案。现在linux中的高性能网络框架(tomcat、netty等)都有epoll的实现。缺点是只有linux支持epoll,BSD内核的kqueue类似于epoll。

2.4 signal driven I/O (SIGIO)
两阶段:
- 等待数据阶段:系统调用sigaction不会阻塞。当数据准备完成之后,会主动的通知用户进程数据已经准备完成,对用户进程做一个回调。
- 数据拷贝阶段: 用户发起的(同步)recvfrom将就绪的数据复制到用户态缓冲区

看到它第一次发起system call不会阻塞进程,kernel的数据就绪后会发送一个signal给进程。进程发起真正的IO操作。
2.5 asynchronous I/O (the POSIX aio_functions)
两阶段:
- 等待数据阶段:系统调用aio_read不会阻塞。
- 数据拷贝阶段: 直到I/O数据准备好内核会直接将数据复制到用户空间,然后内核会给用户进程发送通知,告诉用户进程操作已经完成了。

2.6 总结对比


关键点一:IO分为两个阶段
- 等待数据阶段:用户线程等待内核将数据从网卡拷贝到内核空间。
- 数据拷贝阶段: 内核将数据从内核空间拷贝到用户空间(应用进程的缓冲区)。
关键点二:阻塞非阻塞与同步异步
- 阻塞非阻塞是针对第一阶段来说的,就是等待数据阶段,用户程序是阻塞还是非阻塞的
- 同步异步是针对第二阶段来说的,数据拷贝阶段是是由内核主动发起还是由应用程序来触发。

3.开源组件支持的IO模型
3.1 Tomcat支持的 I/O 模型

注意: Linux 内核没有很完善地支持异步 I/O 模型,因此 JVM 并没有采用原生的 Linux 异步 I/O,而是在应用层面通过 epoll 模拟了异步 I/O 模型。因此在 Linux 平台上,Java NIO 和 Java NIO2 底层都是通过 epoll 来实现的,但是 Java NIO 更加简单高效。
3.2 Java共支持3种网络编程IO模式
3种网络编程IO模式
- BIO(Blocking IO),同步阻塞模型,一个客户端连接对应一个处理线程。
- NIO(Non Blocking IO),同步非阻塞,服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理,JDK1.4开始引入。
- AIO(NIO 2.0),异步非阻塞, 由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用
BIO:
- 缺点
1、IO代码里read操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,浪费资源。
2、如果线程很多,会导致服务器线程太多,压力太大,比如C10K问题。线程的调度、上下文切换乃至它们占用的内存,都会成为瓶颈。 - 应用场景:
BIO 方式适用于连接数目比较小且固定的架构, 这种方式对服务器资源要求比较高, 但程序简单易理解。
NIO:
- 应用场景:
NIO方式适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统, 服务器间通讯,编程比较复杂
AIO:
- 应用场景:
AIO方式适用于连接数目多且连接比较长(重操作)的架构,JDK7 开始支持
3.3 为什么netty还要提供一个基于epoll的实现
自4.0.16起, Netty为Linux通过JNI的方式提供了native socket transport.
- NioEventLoopGroup → EpollEventLoopGroup
- NioEventLoop → EpollEventLoop
- NioServerSocketChannel → EpollServerSocketChannel
- NioSocketChannel → EpollSocketChannel
原因:
- 1)Netty的 epoll transport使用 epoll边缘触发 而 java nio 使用 epoll水平触发(在这个模式下,io来了数据,就只通知这些io设备对应的fd,上次通知过的fd不再通知,内核不用扫描一大堆fd)
- 2)netty epoll transport 暴露了更多的java nio没有的配置参数, 如 TCP_CORK, SO_REUSEADDR等
参考
- 图灵VIP课程
- I/O Models
- linux recvfrom
网友评论