计算机系统应用教程网站

网站首页 > 技术文章 正文

什么同步IO、异步IO、NIO、AIO、阻塞IO、非阻塞IO

btikc 2024-09-14 00:57:32 技术文章 23 ℃ 0 评论

说起IO,是不是脑海里总是浮现一堆概念,什么同步IO、异步IO、NIO、AIO、阻塞IO、非阻塞IO等等,甚至经常还和其他小伙伴争论不休,到底属于什么IO?

其实每种概念都要有一个具体的领域,因为我们的系统主要是运行在Linux环境,比如说我接下来说的IO是基于Linux的网络IO,这是前提。所以我们讨论问题,先把话题场景确定好,每个概念出了这个领域,就完全是另一个概念了,就好比来我们聊聊苹果。你说好用、我说好吃,互不相让。

一、Linux 网络IO

Linux下的网络IO分为5种:阻塞IO(Blocking IO)、非阻塞IO(NonBlocking IO)、IO多路复用(IO Multiplexing)、信号驱动IO(Signal Driven IO)、异步IO(Asynchronous IO)。

看到这些名词总感觉很难区分,我们先要了解几个基本概念,才能把这五种IO好好理解清楚。

用户空间 VS 内核空间

这一块内容相对较多,后续讲零拷贝的时候再详细说,这里就是引入两个概念。

在操作系统中,程序运行的空间分为内核空间和用户空间。应用程序都是运行在用户空间的,所以能操作的数据也都在用户空间。比如应用程序要访问I/O资源必须依赖内核,内核也提供了大量供上层应用访问的接口。

同步 VS 异步

同步:每个请求顺序执行,只有当上一个请求结束时,才能执行下一个请求。在IO层面就是当线程发起一个IO请求,需要等待或者轮询内核IO操作完成后才可以继续请求。

异步:多个请求可以并发执行,不会因为一个请求或者任务导致整个流程等待。IO层面就是当线程发起IO请求后,会继续执行。等到内核IO操作完成会通知用户线程,或者调用线程注册的回调函数。

阻塞 VS 非阻塞

阻塞:发出一个请求,当请求完成所需要的条件不满足时,请求不会满足,直到满足所有条件;

非阻塞:一个请求发出后,当完成请求所需条件不满足时,会返回一个信息告诉请求方不满足条件,不会等待。但是请求结果就需要循环调用请求条件是否满足来获取。

有了上面三个概念,我们接下来用类比的方式理解五种IO

下面是我们去不同餐厅吃饭的场景,一般分为两个步骤,点餐和取餐

1、阻塞IO(Blocking IO)

比如你到包子店买早餐,点完了就站那,等着老板帮你装好,然后你拿走。


应用进程通过系统调用recvfrom接受数据(来个包子和豆浆),但是内核数据报没有准备好,只能阻塞住等待数据(等老板拿包子豆浆),数据报准备好后开始拷贝,拷贝完成返回(拿早餐走人)。

所以这就导致我每天早上买包子排很长的队,效率低,但在这种模型简单,也很容易理解。

2、非阻塞IO(NonBlocking IO)

比如我昨晚吃花甲粉丝,去了点了个口味,老板开始忙活,我点完找个地方坐着玩手机,玩了几分钟,感觉餐好了,去看了下,还没好,回来坐在玩手机,过了一分钟,感觉餐好了,又去看看,还没好。如此反复,直到20分钟后,我的餐终于好了,然后我端着去座位上吃了。

当应用程序通过系统调用recvfrom时(点个香辣口味的花甲粉丝),如果内核中的数据还没有准备好,不再阻塞进程,会立即返回一个error,应用程序可做其他事情,过一段时间再去拿数据(老板没做好,找个座位坐在玩手机)。应用程序为了得到数据,会循环进行系统调用,消耗大量的CPU资源(一遍遍去看看老板做好没),直到拿到数据。

20分钟,一遍遍去查看有没有做好(可以算算我去了多少次),导致我心情很不好。这种模型一般很少直接使用,使用场景偏少,但是为IO多路复用提供了思路。

3、IO多路复用(IO Multiplexing)

还是去餐厅吃饭,这一次专门有个收银员负责点餐,点完餐给我张小票,我找座位去玩手机,餐好了收银员叫我取餐。

对于餐厅老板来说,收银员负责点餐对接,厨师负责做菜,生意好的话,只需要多找几个厨师就行。

IO多路复用会使用select、poll或者epoll函数,下一节会详细介绍,也是本文讲NIO的重点。这几个函数可以同时阻塞多个IO操作(收银员),并且可以检测IO是否可读或者可写(前对接客户,后对接厨师),当有数据可读或者可写时(叫客户取餐或者让厨师做新的订单),才进行recvfrom系统调用进行数据拷贝。

4、信号驱动IO(Signal Driven IO)

这次去的这个店,点好餐老板直接给我一块牌子,我找个地方坐着玩手机,餐好了牌子信号灯会亮,然后我再去取餐。

应用进程预先向内核注册一个信号处理函数(点完餐老板给块牌子),然后用户进程返回,不阻塞,当内核数据准备就绪时会发送一个信号给进程(牌子信号灯亮),才进行recvfrom系统调用进行数据拷贝。

这种模型涉及到了“高科技”,实现比较复杂,并不是每个店都有,所以好多都不支持,比如JAVA的IO里面,就不支持信号驱动IO。

5、异步IO(Asynchronous IO)

吃了这么久的饭,都是要自己取餐的,我不想自己取餐,想想现在的外卖都要自己下楼去取,所以我找了个高级一点的餐厅。

到了餐厅,扫码点餐,点完也不用管,坐在那玩手机,一会儿服务员自动把餐送上餐桌,我负责吃就行。


应用进程调用aio_read 函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回(扫码点餐),当内核将数据拷贝到缓冲区后,再通知应用程序(饭菜上桌)。

总结一下:

当IO发生时,涉及到两块系统,一个是发起调用的应用进程,另一个是内核。

还有两个阶段:

  1. 等待数据准备阶段
  2. 数据从内核拷贝到用户空间


前四种IO都是同步IO,只有最后一种是异步IO。IO多路复用是使用比较广泛,效率很高的IO模型,但是每一种IO都有使用场景,不能要求一个包子店一定要使用IO多路复用。

二、Java的IO

上面讲的五种IO模型,除了含有“高科技”的信号驱动IO模型(Signal Driven IO)JDK没有支持外,其他的都支持。

接下来我们聊聊Java的IO,Java的IO分为三种,分别是:BIO(Blocking IO)、NIO(NonBlocking I/O或者New IO)、AIO(Asynchronous IO)。

首先Java进行网络通信需要一对套接字,分别是服务端的Server Socket和客户端的Client Socket。

流程如上图:

  1. 服务端绑定端口并监听端口
  2. 客户端请求,与服务端建立连接
  3. 数据交互
  4. 关闭连接

1、BIO模型

先写一个简单的服务端伪代码方便理解:

// 服务端
{
       // 创建 ServerSocket 对象并且绑定一个端口
      ServerSocket serverSocket = new ServerSocket(port);


      // 通过 accept()方法监听客户端请求
      Socket socket;
      while ( (socket= serverSocket.accept()) != null)
      {
          // 读数据
          socket.getInputStream();


          // 写数据
          socket.getOutputStream().write(1);
      }
}

这是一个最简单的服务端代码,ServerSocket.accept()是阻塞的,只有客户端发送请求时,才会继续往下走。下面每个socket的读写流程可以使用线程来处理,当请求量大时,可以使用线程池(以上代码不考虑多个客户端并发请求)。

当多个客户端连接时,就会如下图所示,需要为每一个客户端建立一个线程

我们都知道对于服务器来说,线程是宝贵的资源,哪怕用线程池,当客户端数量一上来,服务器资源很快就会被消耗完。

2、NIO模型(重点)

在BIO中,之所以需要大量的线程,是因为没办法获取到可以读写的信息,就像上面我去买早饭一样,只能干等,这就是典型的同步阻塞IO。

为了解决这个问题,JDK1.4新的NIO框架。使用了非阻塞I/O,并且实现了I/O多路复用,也就是的Reactor模式。

在NIO中,我们是先建立一个缓冲区,然后通过管道去读取或者写入数据。

核心变成了Buffer、Channel、Selector,先介绍一下概念

Buffer实质上就是一块内存

// Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;


    // Used only by direct buffers
    // NOTE: hoisted here for speed in JNI GetDirectBufferAddress
    long address;

数据读取操作:


数据写入操作:


接下来的核心就是选择器:


选择器一个对象,可用于监视多个通道的数据到达、连接打开等事件。因此,单线程可以监视多个通道的数据。

下面是一段选择器代码实现,可以跳过,流程下面有

// 1.创建一个ServerSocketChannel和一个Selector,然后把这个server socket 注册到Selector上,并且关注事件为SelectionKey.OP_ACCEPT
ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.socket().bind(new InetSocketAddress("localhost", 8081));
socketChannel.configureBlocking(false);


Selector selector = Selector.open();


socketChannel.register(selector, SelectionKey.OP_ACCEPT);


// 2. 使用select方法阻塞线程,当select返回的时候,唤醒线程,再通过SelectionKeys得到所有可用的channel集合
ByteBuffer writeBuffer = ByteBuffer.allocate(128);
ByteBuffer readBuffer = ByteBuffer.allocate(1024);


writeBuffer.put("test".getBytes());
writeBuffer.flip();


while (true)
{
    // 3. 遍历这个集合,如果其中channel有链接达到,就接收新的连接,然后把这个新的连接注册到selector中
    int n = selector.select();
    Set<SelectionKey> keySet = selector.selectedKeys();


    Iterator<SelectionKey> it = keySet.iterator();


    // 4. 如果channel是读,把关注事情改为写,反之亦然
    while (it.hasNext())
    {
        SelectionKey key = it.next();
        it.remove();


        if (key.isAcceptable())
        {
            SocketChannel channel = socketChannel.accept();


            channel.configureBlocking(false);


            channel.register(selector, SelectionKey.OP_READ);
        }
        else if (key.isReadable())
        {
            SocketChannel channel = (SocketChannel) key.channel();
            readBuffer.clear();
            channel.read(readBuffer);


            readBuffer.flip();


            key.interestOps(SelectionKey.OP_WRITE);
        }
        else if (key.isWritable())
        {
            SocketChannel channel = (SocketChannel) key.channel();
            writeBuffer.rewind();
            channel.write(writeBuffer);


            key.interestOps(SelectionKey.OP_READ);
        }
    }
}

流程:

  1. 创建一个ServerSocketChannel和一个Selector,然后把这个server socket 注册到Selector上,并且关注事件为SelectionKey.OP_ACCEPT
  2. 使用select方法阻塞线程,当select返回的时候,唤醒线程,再通过SelectionKeys得到所有可用的channel集合
  3. 遍历这个集合,如果其中channel有链接达到,就接收新的连接,然后把这个新的连接注册到selector中
  4. 如果channel是读,把关注事情改为写,反之亦然

简单对比:BIO VS NIO

BIO是面向流的,在InputStream、OutputStream中读取一个或者多个字节,直到全部读取完毕。NIO是面向缓冲的,数据先读取到缓冲区,需要处理的时候只需前后移动处理,增加了灵活性。

BIO是阻塞的,read、write方法都是阻塞的,NIO是非阻塞的,期间可以继续干其它事情,所以一个线程可以管理多个输入、输出通道。

NIO的选择器是允许一个线程监视多个通道,可以注册多个通道到同一个选择器上,然后使用一个单独的线程来“选择”已经就绪的通道。这种“选择”机制为一个单独线程管理多个通道提供了可能。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表