一、什么是IO?
我们都知道Liux世界里、一切皆文件、而文件是什么呢?文件就是一串二进制流而已、不管socket、还是FIFO、管道、终端、对我们来说、一切都是文件、一切都是流、在信息交换的过程中、我们都是对这些流进行数据的收发操作、简称为I/O操作(input and output)、往流中读出数据、系统调用read、写入数据、系统调用write、不过话说回来了、计算机里有这么多的流、我怎么知道要操作哪个流呢?做到这个的就是文件描述符、即通常所说的fd、一个fd就是一个整数、所以对这个整数的操作、就是对这个文件(流)的操作、我们创建一个socket、通过系统调用会返回一个文件描述符、那么剩下对socket的操作就会转化为对这个描述符的操作、不能不说这又是一种分层和抽象的思想。
二、IO交互
通常用户进程中的一个完整IO分为两个阶段
用户空间<------------->内核空间、
内核空间<------------->设备空间、
内核空间中存放的是内核代码和数据、而进程的用户空间中存放的是用户程序的代码和数据、不管是内核空间还是用户空间、它们都处于虚拟空间中、Linux使用两级保护机制:0级供内核使用、3级供用户程序使用、操作系统和驱动程序运行在内核空间、应用程序运行在用户空间、两者不能简单地使用指针传递数据、因为Linux使用的虚拟内存机制、其必须通过系统调用请求kernel来协助完成IO动作、内核会为每个IO设备维护一个缓冲区、用户空间的数据可能被换出、当内核空间使用用户空间指针时、对应的数据可能不在内存中。
对于一个输入操作来说、进程IO系统调用后、内核会先看缓冲区中有没有相应的缓存数据、没有的话再到设备中读取、因为设备IO一般速度较慢、需要等待、内核缓冲区有数据则直接复制到进程空间、
所以、对于一个网络输入操作通常包括两个不同阶段:
(1)等待网络数据到达网卡 –> 读取到内核缓冲区
(2)从内核缓冲区复制数据 –> 用户空间
IO有内存IO、网络IO和磁盘IO三种、通常我们说的IO指的是后两者。
阻塞IO(blocking I/O)
我们运行一段服务端socket监听程序(典型的阻塞IO场景):
我们知道server.accept是阻塞的,如果没有连接连上来就会一直等待不会往下执行。
同时我们是道reader.readLIne也是阻塞的,不写入东西也不会往下执行。所以我们new了个线程,可以达到同时监听多个连接的目的。
其实网络通信过程中的系统调用:前面两个函数的阻塞的根因是因为内核的accept和recv的系统调用是阻塞调用,所以会有BIO。
这段程序中涉及到的系统调用如下:
java的bio对应的包是:java.io.*
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
2.非阻塞IO(noblocking I/O)
java的nio对应的:java.nio.*(jdk1.4之后才有)
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
下面是一段典型的java nio服务端的代码:其中
ss.configureBlocking(false)//很重要,表示设置为非阻塞io
ss.accept在非阻塞模式下不会阻塞,
- 非阻塞模式:在调用accept方法后,如果无连接建立,则返回
null(实际上系统调用的返回时-1,java返回时null)
;如果有连接,则返回SocketChannel。
我们就达到一个线程监听多个请求的作用。前面的BIO需要多个线程才能同时监听到多个请求。
SocketChannel简述:ServerSocketChannel简述_weixin_33951761的博客-CSDN博客
注意for循环需要遍历所有连接,向内核发送recv系统调用,系统调用会产生软中断造成用户态内核态上下文切换,有很多无效系统调用。那怎么很容易想到减少系统调用的次数。
所以我们有了多路复用器,进程发生系统调用前,先去查下有多少个可以读:
IO多路复用
目前支持I/O多路复用的系统调用有 select,pselect,poll,epoll
,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作
。epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定
,一般操作系统均有实现。关于io多路复用可参考此文:网络通信 --> IO多路复用之select、poll、epoll详解 - 蚂蚁吃大象、 - 博客园
对于 select 这种⽅式,需要进⾏ 2 次「遍历」⽂件描述符集合,⼀次是在内核态⾥,⼀个次是在⽤户态⾥ ,⽽且还会发⽣ 2 次「拷⻉」⽂件描述符集合,先从⽤户空间传⼊内核空间,由内核修改后,再传出到⽤户空间中。
高版本的jdk主要是用的是epoll系统调用:epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次
。
基本原理:epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次
。还有一个特点是,epoll使用“事件”的就绪通知方式
,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd
,epoll_wait便可以收到通知。Epoll对于多核来说很友好,相对于前面两个系统调用,多了create和ctl,意味这不用每次都传很多的文件描述符,但是在内核里面增加了两块空间。空间换时间的做法。
java实际上用的是操作系统的系统调用来实现的自己的多路复用,也就是操作系统的多路复用是java的多路复用的基础。
Reactor模型
刚刚的select和epoll都是关注某个io连接产生的事件。但是实际上,我们在处理io的时候往往最关注的有相关业务的处理,并且我们关注的也不是网络io的处理,而是之关心某个事件触发然后执行相应的业务逻辑。所以我们需要封装一层,将事件和IO多路复用抽象出来,我们自己可以选择自己的实现,来方便我们进行IO编程。使得的IO编程更加灵活。
组成:阻塞IO+IO多路复用
特征:以事件循环、事件驱动、事件回调来实现业务逻辑处理
Reactor模式的抽象:
a, Handle表示句柄,文件描述符、socket等; 实际上就是对IO事件的抽象。实际上就是对fd进行了包装。
b, EventDemultiplexer表示多路分发机制,调用系统提供的多IO路复用,比如select,epoll。 程序先将关注的句柄注册到EventDemultiplexer,当有相关事件到来触发EventDemultiplexer通知程序。
c, EventHandler定义事件处理方法,
d, Reactor是事件管理的接口,注册和销毁事件,并运行事件循环,当EventDemultiplexer返回Handle有事件"就绪",将其分发给EventHandler上对应的方法。
e, ConcreteEventhandler实现每个事件的处理逻辑。
Netty是典型的Reactor模型结构,关于Reactor的详尽阐释,本文站在巨人的肩膀上,借助 Doug Lea(就是那位让人无限景仰的大爷)的“Scalable IO in Java”中讲述的Reactor模式。
“Scalable IO in Java”的地址是:http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
此文来自于网课记录一下
历程: