linux中的五大IO模型和java的三种IO模型
IO模型
之前经常听到五种io模型和三种io模型的说法,经常容易弄混淆,这里详细讲解下。
五大IO模型:Linux系统中的五种io模型,分别是:阻塞io、非阻塞io、多路复用io,信号驱动io和异步io。
三种IO模型:JAVA种的三种io模型,分别是:同步阻塞(BIO),同步非阻塞(NIO)和异步非阻塞模型(AIO)。
背景
IO在计算机中指Input/Output,也就是输入和输出。由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。
I/O操作是相对于内存而言的,从外部设备进入内存就叫Input,反之从内存输出到外部设备就叫Output。
LINUX中进程无法直接操作I/O设备,必须通过系统调用,请求kernel来协助完成I/O动作。内核会为每个I/O设备维护一个缓冲区。
对于一个输入操作来说,进程IO系统调用后,内核会先看缓冲区中有没有相应的缓存数据,有数据则直接复制到进程空间,没有的话再到设备中读取,因为IO设备一般速度较慢,需要等待。
通常用户进程中的一个完整IO分为两阶段:用户进程空间<–>内核空间、内核空间<- ->设备空间(磁盘、网络等)。IO有内存IO、 网络IO和磁盘IO三种,通常我们说的IO指的是后两者。
I/O按照设备来分的话,分为两种:一种是网络I/O,也就是通过网络进行数据的拉取和输出。一种是磁盘I/O,主要是对磁盘进行读写工作。
1、内核态,用户态
想要弄懂 IO 模型,需要了解内核态和用户态的概念。 操作系统为了保护自己,设计了用户态、内核态两个状态。应用程序一般工作在用户态,当调用一些底层操作的时候(比如 IO 操作),就需要切换到内核态才可以进行。用户态和内核态的切换需要消耗一些资源,零拷贝技术就是通过减少用户态和内核态的转换来提高性能的。
虚拟内存被操作系统划分成两块:内核空间和用户空间。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。
内核空间是内核代码运行的地方,用户空间是用户程序代码运行的地方。
当进程运行在内核空间时就处于内核态,当进程运行在用户空间时就处于用户态。
2、应用程序从网络中接收数据的大致流程
服务器从网络接收的大致流程如下:
1、数据通过计算机网络来到了网卡
2、把网卡的数据读取到 socket 缓冲区
3、把 socket 缓冲区读取到用户缓冲区,之后应用程序就可以使用了
核心就是两次读取操作,五大 IO 模型的不同之处也就在于这两个读取操作怎么交互。
3、同步/异步、阻塞/非阻塞
同步/异步:这个是应用层面的概念,指的是调用一个函数,我们是等这个函数执行完再继续执行下一步,还是调完函数就继续执行下一步,另起一个线程去执行所调用的函数。关注的是线程间的协作。
同步和异步关注的是消息通信机制。
所谓同步,就是在发出一个调用时,自己需要参与等待结果的过程,则为同步,前面四个IO都自己参与了,所以也称为同步IO.
异步IO,则指出发出调用以后,到数据准备完成,自己都未参与,则为异步.
阻塞/非阻塞,这个是硬件层面的概念,阻塞是指 cpu “被”休息,处理其他进程去了,比如IO操作,而非阻塞则是 cpu 仍然会执行,不会切换到其他进程。关注的是CPU会不会“被”休息,表现在应用层面就是线程会不会“被”挂起。
至于同步和阻塞有什么区别,异步和非阻塞有什么区别,其实这是不同层面的东西,不好相互比较的。
Linux五大IO模型
之前提了,应用程序从网络中接收数据的大致流程就是两步:
- 数据准备:等待网络数据,把网卡的数据读取到 socket 缓冲区
- 数据复制:把 socket 缓冲区的数据读取到用户态 Buffer,供应用程序使用
阻塞IO
学术语言就是:在应用调用recvfrom读取数据时,其系统调用直到数据包到达且被复制到应用缓冲区中或者发送错误时才返回,在此期间一直会等待,进程从调用到返回这段时间内都是被阻塞的称为阻塞IO;在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式
流程描述:
1、应用进程向内核发起recfrom读取数据
2、内核进行准备数据报(此时应用进程阻塞)
3、内核将数据从内核负复制到应用空间。
4、复制完成后,返回成功提示
非阻塞IO
非阻塞IO就是当应用B发起读取数据申请时,如果内核数据没有准备好会即刻告诉应用B,不会让B在这里等待,如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码;
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一 般只有特定场景下才使用
流程和流程图:
1、应用进程向内核发起recvfrom读取数据。
2、内核数据报没有准备好,即刻返回EWOULDBLOCK错误码。
3、应用进程再次向内核发起recvfrom读取数据。
4、内核倘若已有数据包准备好就进行下一步骤,否则还是返回错误码,执行第三步骤
5、内核将数据拷贝到用户空间。
6、完成后,返回成功提示。
多路复用IO
多路复用模型: 由一个线程监控多个网络请求(fd文件描述符,linux系统把所有网络请求以一个fd来标识),来完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这样就可以节省出大量的线程资源出来。
上图可以看出多路复用就是系统提供了一种函数可以同时监控多个fd的操作,这个函数就是我们常说到的select、poll、epoll函数,可以通过它们同时监控多个fd,只要有任何一个数据状态准备就绪了,就返回可读状态,这时询问线程再去通知处理数据的线程,对应线程此时再发起recvfrom请求去读取数据.
虽然从流程图上看起来和阻塞IO类似.,实际上最核心之处在于IO多路转接能够同时等待多个文件 描述符的就绪状态,来达到不必为每个fd创建一个对应的监控线程,从而减少线程资源创建的目的。
流程图:
信号驱动IO
多路转接解决了一个线程可以监控多个fd的问题,但是select采用无脑的轮询就显得有点暴力,因为大部分情况下的轮询都是无效的,所以有人就想,别让我总去问数据是否准备就绪,而是等你准备就绪后主动通知我,这边是信号驱动IO.
信号驱动IO是在调用sigaction时候建立一个SIGIO的信号联系,当内核准备好数据之后再通过SIGIO信号通知线程,此fd准备就绪,当线程收到可读信号后,此时再向内核发起recvfrom读取数据的请求,因为信号驱动IO的模型下,应用线程在发出信号监控后即可返回,不会阻塞,所以一个应用线程也可以同时监控多个fd。
内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作.
多路复用IO里面的select虽然可以监控多个fd了,但select其实现的本质上还是通过不断的轮询fd来监控数据状态, 因为大部分轮询请求其实都是无效的,所以信号驱动IO意在通过这种建立信号关联的方式,实现了发出请求后只需要等待数据就绪的通知即可,这样就可以避免大量无效的数据状态轮询操作。
异步IO
异步IO 则和上面四种IO模型都不通,他是完完全全的异步,两步操作都不会阻塞。 应用程序发起 read 调用后,等收到回调通知,就可以去使用用户态 Buffer 的数据了。
应用只需要向内核发送一个读取请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核会发起一个通知告诉应用,我们称这种模式为异步IO模型。
异步IO的优化思路是解决应用程序需要先后发送询问请求、接收数据请求两个阶段的模式,在异步IO的模式下,只需要向内核发送一次请求就可以完成状态询问和数拷贝的所有操作。
JAVA的三种IO模型
BIO(同步阻塞)
同步阻塞模型,一个客户端连接对应一个处理线程
对于每一个新的网络连接都会分配给一个线程,每隔线程都独立处理自己负责的输入和输出, 也被称为Connection Per Thread模式
缺点:
1、IO代码里read操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,浪费资源
2、如果线程很多,会导致服务器线程太多,压力太大,比如C10K问题
所谓c10k问题,指的是服务器同时支持成千上万个客户端的问题,也就是concurrent 10 000 connection
应用场景: BIO 方式适用于连接数目比较小且固定的架构, 这种方式对服务器资源要求比较高, 但程序简单易理解。
示例代码如下:
/**
* @Title:BIO的服务端
*/
public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9000);
while (true){
System.out.println("等待连接...");
Socket clientSocket = serverSocket.accept();
System.out.println("客户端"+clientSocket.getRemoteSocketAddress()+"连接了!");
handle(clientSocket);
}
}
private static void handle(Socket clientSocket) throws IOException{
byte[] bytes = new byte[1024];
int read = clientSocket.getInputStream().read(bytes);
System.out.println("read 客户端"+clientSocket.getRemoteSocketAddress()+"数据完毕");
if(read != -1){
System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
}
clientSocket.getOutputStream().write("HelloClient".getBytes());
clientSocket.getOutputStream().flush();
}
}
/**
* @Title:BIO的客户端
*/
public class SocketClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 9000);
//向服务端发送数据
socket.getOutputStream().write("HelloServer".getBytes());
socket.getOutputStream().flush();
System.out.println("向服务端发送数据结束");
byte[] bytes = new byte[1024];
//接收服务端回传的数据
socket.getInputStream().read(bytes);
System.out.println("接收到服务端的数据:" + new String(bytes));
socket.close();
}
}
NIO(同步非阻塞)
同步非阻塞,服务器实现模式为 一个线程可以处理多个连接请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器轮询到连接有IO请求就进行处理,是在JDK1.4开始引入的。
应用场景:NIO方式适合连接数目多且连接比较短(轻操作)的架构,比如聊天服务器、弹幕系统、服务器之间通讯,编程相对复杂。netty就是nio模型。
NIO 有三大核心组件: Channel(通道), Buffer(缓冲区),Selector(多路复用器)
1.channel类似于流,每个channel对应一个buffer缓冲区,buffer底层就是个数组
2.channel 会注册到selector上,由selector根据channel读写事件的发生将其交由某个空闲的线程处理
3.NIO的Buffer和Channel都是可读也可写的。
NIO的代码示例有两个:
没有引入多路复用器的NIO:
/**
* @Title:Nio服务端
*/
public class NioServer {
/**
* 保存客户端连接
*/
static List<SocketChannel> channelList = new ArrayList<>();
public static void main(String[] args) throws IOException {
//创建Nio ServerSocketChannel
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(9000));
//设置ServerSocketChannel为非阻塞
serverSocket.configureBlocking(false);
System.out.println("Nio服务启动成功");
while(true){
//非阻塞模式accept方法不会阻塞
/// NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数
SocketChannel socketChannel = serverSocket.accept();
if(socketChannel != null){
System.out.println("连接成功");
socketChannel.configureBlocking(false);
channelList.add(socketChannel);
}
Iterator<SocketChannel> iterator = channelList.iterator();
while(iterator.hasNext()){
SocketChannel sc = iterator.next();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
//非阻塞模式read方法不会阻塞
int len = sc.read(byteBuffer);
if(len > 0){
System.out.println("接收到消息:" + new String(byteBuffer.array()));
}else if(len == -1){
iterator.remove();
System.out.println("客户端断开连接");
}
}
}
}
}
/**
* @Title:Nio客户端
*/
public class NioClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("localhost", 9000));
socketChannel.configureBlocking(false);
ByteBuffer writeBuffer=ByteBuffer.wrap("HelloServer1".getBytes());
socketChannel.write(writeBuffer);
System.out.println("向服务端发送数据1结束");
writeBuffer = ByteBuffer.wrap("HelloServer2".getBytes());
socketChannel.write(writeBuffer);
System.out.println("向服务端发送数据2结束");
writeBuffer = ByteBuffer.wrap("HelloServer3".getBytes());
socketChannel.write(writeBuffer);
System.out.println("向服务端发送数据3结束");
}
}
引入了多路复用器的NIO:
/**
* @Title:引入多路复用器后的NIO服务端
* SelectionKey.OP_ACCEPT —— 接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
* SelectionKey.OP_CONNECT —— 连接就绪事件,表示客户与服务器的连接已经建立成功
* SelectionKey.OP_READ —— 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
* SelectionKey.OP_WRITE —— 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)
*
* 1.当向通道中注册SelectionKey.OP_READ事件后,如果客户端有向缓存中write数据,下次轮询时,则会 isReadable()=true;
*
* 2.当向通道中注册SelectionKey.OP_WRITE事件后,这时你会发现当前轮询线程中isWritable()一直为true,如果不设置为其他事件
*/
public class NioSelectorServer {
public static void main(String[] args) throws IOException {
/**
* 创建server端,并且向多路复用器注册,让多路复用器监听连接事件
*/
//创建ServerSocketChannel
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(9000));
//设置ServerSocketChannel为非阻塞
serverSocket.configureBlocking(false);
//打开selector处理channel,即创建epoll
Selector selector = Selector.open();
//把ServerSocketChannel注册到selector上,并且selector对客户端的accept连接操作感兴趣
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NioSelectorServer服务启动成功");
while(true){
//阻塞等待需要处理的事件发生
selector.select();
//获取selector中注册的全部事件的SelectionKey实例
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
//遍历selectionKeys,对事件进行处理
while (iterator.hasNext()){
SelectionKey key = iterator.next();
//如果是OP_ACCEPT事件,则进行连接和事件注册
if(key.isAcceptable()){
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
//接受客户端的连接
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
//把SocketChannel注册到selector上,并且selector对客户端的read操作(即读取来自客户端的消息)感兴趣
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端"+socketChannel.getRemoteAddress()+"连接成功!");
}else if(key.isReadable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int len = socketChannel.read(byteBuffer);
if(len > 0){
System.out.println("接收到客户端"+socketChannel.getRemoteAddress()+"发来的消息,消息内容为:"+new String(byteBuffer.array()));
}else if(len == -1){
System.out.println("客户端断开连接");
//关闭该客户端
socketChannel.close();
}
}
//从事件集合里删除本次处理的key,防止下次select重复处理
iterator.remove();
}
}
}
}
/**
* @Title:引入多路复用器后的NIO客户端
*/
public class NioSelectorClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
//要先向多路复用器注册,然后才可以跟服务端进行连接
socketChannel.register(selector, SelectionKey.OP_CONNECT);
socketChannel.connect(new InetSocketAddress("localhost", 9000));
while (true){
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if (key.isConnectable()){
SocketChannel sc = (SocketChannel) key.channel();
if (sc.finishConnect()){
System.out.println("服务器连接成功");
ByteBuffer writeBuffer=ByteBuffer.wrap("HelloServer".getBytes());
sc.write(writeBuffer);
System.out.println("向服务端发送数据结束");
}
}
}
}
}
}
AIO(异步非阻塞)
异步非阻塞,由操作系统完成后回调通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
应用场景:AIO方式适用于连接数目多且连接时间较长(重操作)的架构(应用),JDK7开始支持。
著名的异步网络通讯框架netty之所以废弃了AIO,原因是:在Linux系统上,NIO的底层实现使用了Epoll,而AIO的底层实现仍使用Epoll,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化,Linux上AIO还不够成熟
AIO示例代码如下:
/**
* @Title:Aio服务端
*/
public class AioServer {
public static void main(String[] args) throws Exception {
final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
try{
System.out.println("2--"+Thread.currentThread().getName());
//接收客户端连接
serverChannel.accept(attachment,this);
System.out.println("客户端"+socketChannel.getRemoteAddress()+"已连接");
ByteBuffer buffer = ByteBuffer.allocate(128);
socketChannel.read(buffer, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
System.out.println("3--"+Thread.currentThread().getName());
//flip方法将Buffer从写模式切换到读模式
//如果没有,就是从文件最后开始读取的,当然读出来的都是byte=0时候的字符。通过buffer.flip();这个语句,就能把buffer的当前位置更改为buffer缓冲区的第一个位置
buffer.flip();
System.out.println(new String(buffer.array(), 0, result));
socketChannel.write(ByteBuffer.wrap("hello Aio Client!".getBytes()));
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
}catch(Exception e){
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
}
});
System.out.println("1‐‐main"+Thread.currentThread().getName());
Thread.sleep(Integer.MAX_VALUE);
}
}
/**
* @Title:Aio客户端
*/
public class AioClient {
public static void main(String[] args) throws Exception {
//创建Aio客户端
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 9000)).get();
//发送消息
socketChannel.write(ByteBuffer.wrap("hello AIO server !".getBytes()));
//接收消息
ByteBuffer buffer = ByteBuffer.allocate(128);
Integer len = socketChannel.read(buffer).get();
if(len != -1){
//客户端收到消息:hello Aio Client!
System.out.println("客户端收到消息:"+new String(buffer.array(), 0, len));
}
}
}
参考:
1、https://blog.youkuaiyun.com/m0_51723227/article/details/127931476
2、https://www.w3cschool.cn/article/40045541.html