0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

IO如何实现

科技绿洲 来源:了不起 作者:了不起 2023-09-25 10:57 次阅读

IO模型

我们的程序基本上都是对数据的IO操作以及基于CPU的运算。

基于Java的开发大部分是网络相关的编程,不管是基于如Tomcat般的Web容器,或是基于Netty开发的应用间的RPC服务。为了提供系统吞吐量, 降低硬件资源的开销,IO模型也在不断适应大规模、高并发需求不断演进,今天我们就来看看这个在网络上高频出现的词汇IO模型

linux IO模型

首先我们要明确,用户程序从计算机硬件读取数据(包括文件、网络数据等),会经历数据从硬件设备中读取到系统内核后,再拷贝到用户空间的过程。在linux系统中,针对这一操作提供了5种IO模型用于优化不同场景下的IO操作。

  • 同步阻塞IO 系统程序调用recvfrom阻塞等待内核将数据准备(从网卡将数据读取到内存中)。之后用户通过recvfrom等待内核将数据准备好,此时内核将数据从内核缓冲区复制到用户态缓冲区。

图片

blocking I/O发起system call recvfrom()时,进程将一直阻塞等待另一端Socket的数据到来。在该模式下,会阻塞其他连接的建立,因此一般都会通过多线程处理Socket数据的读取。

Blocking I/O优点是简单易用,对于本地I/O而言性能很高。缺点是处理网络I/O时,造成进程阻塞,以及创建线程的资源消耗。

  • 同步非阻塞IO
    系统程序调用recvfrom时并不会阻塞等待,但是需要调用方不停的去轮询内核,获取数据准备状态。之后用户发起的(同步)recvfrom检查到内核将数据准备好后,进行数据由内核到用户空间的复制。

图片

相对于阻塞I/O的等待,非阻塞I/O隔一段时间就就需要发起system call判断数据是否就绪。如果数据就绪,就从kernel space复制到user space,操作数据; 否则,kernel会立即返回EWOULDBLOCK这个错误。

recvfrom有个参数叫flags,默认情况下阻塞。可以设置flag为非阻塞让kernel在数据未就绪时直接返回。这就是”非阻塞”主要是指数据准备阶段。

  • IO多路复用
    系统程序调用select/poll/epoll会阻塞等待至少有一个套接字就绪则返回。用户(同步)调用recvfrom,获取这些就绪的套接字,轮询将数据由内核复制到用户态缓冲区。

图片

I/O Multiplexing首先向kernel发起system call,传入file descriptor和感兴趣的事件(readable、writable等)让kernel监测, 当其中一个或多个fd数据就绪,就会返回结果。程序再发起真正的I/O操作recvfrom读取数据。

  • 信号驱动IO
    系统调用sigaction不会阻塞。当数据准备完成之后,会主动的通知用户进程数据已经准备完成,对用户进程做一个回调。用户发起的(同步)recvfrom将就绪的数据由内核复制到用户态缓冲区。

图片

第一次发起system call不会阻塞进程,kernel的数据就绪后会发送一个signal给进程。进而发起真正的IO操作。

  • 异步IO
    系统调用aio_read不会阻塞。直到I/O数据准备好内核会直接将数据复制到用户空间,然后内核主动会给用户进程发送通知,告诉用户进程信号表示并进行数据处理。

图片

既然说到异步IO,则前面的几种IO模型都是同步的,由上图可以看到,在数据拷贝(内核态到用户态)时,仍然是阻塞的。在异步IO中,请求连接到内核后,从数据准备到复制整个过程 都是在内核中完成,对应用户程序不会阻塞,直到请求数据完全准备好后,通过回调函数通知用户程序完成整个IO操作。

Java中的IO模型

Java中提供的IO相关的API,主要是基于操作系统底层的IO的操作。在Java中的BIO、NIO、AIO属于Java对操作系统的各种IO模型的封装。当我们使用这些API时,不用关注底层IO的实现。

  • BIO

同步阻塞IO ,服务端通过阻塞输入流来监听客户端是否有数据写入,当处理输入数据时,程序会等待内核完成处理完成并返回后才会继续执行。

图片

上图可以看到,服务端通过ServerSocket#accept阻塞方法监听客户端的接入,然后阻塞在通过阻塞输入流等待客户端的输入,如果一直没有输入,则其他客户端都会被阻塞在此。

图片

我们可以通过多线程来改善,每个客户端连接时,都由独立的线程来处理,虽然通过多线程可以解决客户端间的阻塞问题,但单个线程内然是阻塞模式, 并且当客户端过多时需要足够的线程来支持,比较耗费系统资源。

图片

  • NIO

同步非阻塞IO ,基于多路复用模型,依赖于服务器操作系统,通过一个Selector即可监听多个连接,并进行IO处理。但要注意,如果处理IO的过程较长一样会影响到其他的连接。

图片

服务端通过Selector#select阻塞方法,监听Channel状态,一旦有Channel准备就绪,程序才会继续往下执行,因此需要不断轮询并监控Channel的状态变更。与BIO的多线程模式非常相似,只不过BIO是基于多线程技术实现,而NIO是基于操作系统底层提供的函数,效率更好且资源消耗更少。

图片

  • AIO

异步非阻塞IO ,在JDK1.7之后提供了异步的相关Channel,AIO提供异步功能, 基于回调函数实现 ,同样依赖于操作系统底层的异步IO模型,异步操作的实现是在对应的 accept、connection、read、write等方法异步执行,完成后会主动调用回调函数。

图片

其中accept、read等方法都是非阻塞的,即立即返回结果,几乎所有的异步操作都是基于回调函数实现,这种方式不管是对操作系统资源的利用以及效率上都是最佳的实现。

图片

虽然三种IO模型的演进是为了提升系统处理IO的能力,但是开发的复杂度也同步上升:

  • BIO方式适用于连接数目比较小且固定的架构,需要依赖于线程来支持多个客户端接入,但程序直观简单易理解。
  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。
  • AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂。

同/异步与(非)阻塞

关于阻塞、非阻塞、同步、异步这些名词的解释,可以在网上找到很多解释,但是如何能够从本质上描述其含义,正如IO与NIO中说到的阻塞与非阻塞,又是怎么体现的呢?

我们一般说说的IO模型,其实是服务端进行IO操作执行与实现的形式,程序将数据从程序写入或读写时,与硬件设备(比如硬盘、网卡)间,基于操作系统提供的系统api实现数据由用户态与内核态交互的一种形式。

  • 同步
    程序执行需要等待返回后才会继续。
  • 异步
    与同步相反,比较直观的就是线程。
  • 阻塞IO
    程序需要等待内核IO操作完成后返回到用户空间继续执行用户程序的操作指令。这里的阻塞主要是调用操作系统api被阻塞导致程序挂起,描述的是程序当前执行的状态。
  • 非阻塞IO
    既然阻塞是调用操作系统api被阻塞,那么非阻塞则相反,得益于操作系统提供的函数支持,一般是通过轮询机制与回调函数实现。

同步与异步属于程序发起请求的方式;阻塞与非阻塞属于服务响应IO操作的底层实现方式。

示例

基于上面的理解,我们看下在Java中如何实现BIO、NIO以及AIO。

BIO

Server:

serverSocket = new ServerSocket(port);
  // 阻塞直到有连接
  Socket clientSocket = serverSocket.accept();
  // 阻塞读取数据
  BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  log.info(" >> >> > Server接收消息:{}" , reader.readLine());
  socket.shutdownInput();
  
  log.info(" >> >> > Server回复消息:{}" , message);
  PrintWriter writer = new PrintWriter(socket.getOutputStream());
  writer.println(message);

Client:

// 连接服务端
  socket = new Socket("127.0.0.1",port);
  OutputStream out = socket.getOutputStream();
  out.write(message.getBytes());
  socket.shutdownOutput();
  
  BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  log.info("接收Server回复:{}", reader.readLine());

NIO

省略

AIO

Server:

//
    serverSocketChannel = AsynchronousServerSocketChannel.open();
    //绑定端口
    serverSocketChannel.bind(new InetSocketAddress(port));
    //异步接收客户端连接
    serverSocketChannel.accept(null, new AcceptCompletionHandler< String >());

    /**
     * 处理客户端连接
     * @param < T >
     */
    public class AcceptCompletionHandler< T > implements CompletionHandler< AsynchronousSocketChannel,T > {

        @Override
        public void completed(AsynchronousSocketChannel result, T attachment) {
            log.info(" >> > 客户端接入...");
            ByteBuffer byteBuffer = ByteBuffer.allocate(512);
            //异步读客户端数据
            result.read(byteBuffer, byteBuffer, new ReadCompletionHandler());
            //接收其他的客户端连接的
            serverSocketChannel.accept(null, this);
        }

        @Override
        public void failed(Throwable exc, T attachment) {
            log.error(" >> > 客户端接入失败:{}", exc.getMessage());
        }
    }

    /**
     * 处理ServerChannel读取
     * @param < T >
     */
    public class ReadCompletionHandler< T extends Buffer > implements CompletionHandler< Integer, T >{

        @Override
        public void completed(Integer result, T attachment) {
            if(attachment.hasRemaining()){
                // 切换成读模式
                attachment.flip();
                //
                if( attachment instanceof ByteBuffer ){
                    byte[] bytes = new byte[attachment.remaining()];
                    ((ByteBuffer)attachment).get(bytes); // 从Buffer中取数据 get
                    log.info("Server接收消息:{}", new String(bytes));
                }
            }
        }

        @Override
        public void failed(Throwable exc, T attachment) {
            log.error("Server接收消息失败:{}", exc.getMessage());
        }
    }

Client:

//创建异步通道实例
    socketChannel = AsynchronousSocketChannel.open();
    //连接服务端,异步方式
    socketChannel.connect(new InetSocketAddress("127.0.0.1",port), null, new ConnetionComplateHandler());
    // 消息发送
    this.socketChannel.write(Charset.defaultCharset().encode(message));
    /**
     *
     * @param < T >
     */
    public class ConnetionComplateHandler< T > implements CompletionHandler< Void, T > {

        @Override
        public void completed(Void result, T attachment) {
            log.info("Client连接服务的成功...");
        }

        @Override
        public void failed(Throwable exc, T attachment) {

        }
    }

结束语

通过了解操作系统层面的IO模型可以让我们理解IO是如何实现,以及通过Java语言提供的类库实现了操作系统底层API调用的复杂性。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • IO
    IO
    +关注

    关注

    0

    文章

    433

    浏览量

    39037
  • 数据
    +关注

    关注

    8

    文章

    6792

    浏览量

    88725
  • Linux系统
    +关注

    关注

    4

    文章

    590

    浏览量

    27303
  • 程序
    +关注

    关注

    116

    文章

    3754

    浏览量

    80725
收藏 人收藏

    评论

    相关推荐

    51单片机总线扩IO如何实现

    51单片机工作在总线方式,准备用1个8位锁存器扩充8个输出口,要求上电时,输出口必须保证为高电平,系统复位时(如按下复位键不放),也要确保输出口为高电平,电路如何实现,问题的关键是上电状态如何为高。
    发表于 09-30 02:24

    怎么用8个IO实现8个波形?

    要用8个IO实现8个波形,8路信号同时进行的,具体波形见附件中的文件,跪求大神指导指导,给个思路,谢谢。OSC.zip (62.49 KB )
    发表于 03-08 20:16

    ds1302时钟芯片dat由低位到高位写入io怎么实现

    命令dat由低位到高位写入io中,也就是是命令写入函数 。 正确的理解是例如 dat=0x45即dat=0100 0101和0x01进行‘’“&”运算, i=0时,屏蔽掉前七位 即0100
    发表于 04-30 01:35

    Xilinx7系列IO实现差分信号

    都可配置成输入、输出。每个bank的首尾管脚只能作为单端I/O,其余48个I/O则可配置成24对差分I/O。在差分信号的实现过程中,管脚分配应选择相应电平标准的bank中除首尾以外的其他48个IO
    发表于 12-23 17:17

    基于ARM的地铁用安全型智能IO的设计与实现

    地铁信号设备中输入输出设备是信号逻辑和现场设备之间的接口,有着四高(高安全,高可靠,高可维护,高可用)要求,目前信号系统厂家的传统做法是整个信号系统产品由一家
    发表于 10-27 15:48 12次下载

    基于PROFINET IO实现S7-1200与S120通讯

    S7-1200 与SINAMICS S120 之间通过PROFINET IO 可进行周期性或: 非周期性数据通讯,使用功能块DPWR_DAT/DPRD_DAT,S7-1200 通过PROFINET
    发表于 09-29 16:50 30次下载
    基于PROFINET <b class='flag-5'>IO</b><b class='flag-5'>实现</b>S7-1200与S120通讯

    利用到电容充放电原理实现一条IO实现两个按键

    .MCU_IO1设定为输入,如果J1、J2均不按下,此时MCU_IO1可以理解成一个阻值很大的电阻接地,电容C1上的电荷会通过这个电阻逐渐释放掉,这样C1上的电压会逐渐降低到零。因为C1上的电压
    的头像 发表于 03-26 08:39 8812次阅读
    利用到电容充放电原理<b class='flag-5'>实现</b>一条<b class='flag-5'>IO</b><b class='flag-5'>实现</b>两个按键

    单片机应用系统中如何通过IO实现断电自关机?通过电路图给你讲解

    单片机应用系统中,常有用单片机的IO口来实现自关机(彻底关机)的功能。一般用单片机的一个IO口控制一个电子开关来实现,因单片机关电后,失去电源,所以在关机时,
    的头像 发表于 09-24 11:45 7041次阅读
    单片机应用系统中如何通过<b class='flag-5'>IO</b><b class='flag-5'>实现</b>断电自关机?通过电路图给你讲解

    MAXREFDES150 Pocket IO如何提高PLC平台效率

    了解MAXREFDES150 Pocket IO™如何实现比之前的PLC平台效率提高30%,体积减小2½倍。Pocket IO是完备的工业平台,配备有30路IO,具有三种不同的传感器输
    的头像 发表于 10-11 03:07 3356次阅读

    使用Google Assistant和Adafruit IO实现家庭自动化

    电子发烧友网站提供《使用Google Assistant和Adafruit IO实现家庭自动化.zip》资料免费下载
    发表于 12-05 09:57 0次下载
    使用Google Assistant和Adafruit <b class='flag-5'>IO</b><b class='flag-5'>实现</b>家庭自动化

    FPGA数字IO如何实现DAC功能

    假设方波频率为f0。横轴谐波次数为0的柱状图代表直流分量的幅值,也就是方波的平均电压(与占空比有关),谐波次数为1代表频率为f0的正弦波分量的幅值,3代表,3*f0的正弦波分量幅值,以此类推。
    的头像 发表于 12-07 10:31 1518次阅读

    求一种Ti60F100高速IO实现HDMI环出方案

    xilinx和altera都有通过IO驱动HDMI的方案,支持的分辨率各有不同,这跟不同系列的FPGA性能是相关的。
    的头像 发表于 03-09 14:20 2335次阅读
    求一种Ti60F100高速<b class='flag-5'>IO</b><b class='flag-5'>实现</b>HDMI环出方案

    请问FPGA数字IO如何实现DAC功能呢?

    假设方波频率为f0。横轴谐波次数为0的柱状图代表直流分量的幅值,也就是方波的平均电压(与占空比有关),谐波次数为1代表频率为f0的正弦波分量的幅值,3代表,3*f0的正弦波分量幅值,以此类推。
    发表于 06-28 14:50 490次阅读
    请问FPGA数字<b class='flag-5'>IO</b>如何<b class='flag-5'>实现</b>DAC功能呢?

    气压调节远程IO实现制冷空调智能控制的重要技术

    应用介绍 本项目为大型制冷系统的气压调节项目,本次应用工位为项目配套电气控制柜。客户使用西门子PLC,原IO系统使用西门子远程IO:ET200SP系列,本项目因受到西门子货期影响的机会,使我们得到
    的头像 发表于 10-31 11:40 473次阅读
    气压调节远程<b class='flag-5'>IO</b>:<b class='flag-5'>实现</b>制冷空调智能控制的重要技术

    远程IO实现设备间高效通信与控制的桥梁

    在当今数字化时代,远程IO(输入/输出)技术已成为实现工业自动化、智慧城市等系统中不可或缺的一部分。那么,远程IO究竟是什么?它又是如何工作的呢?今天,我将带您探索远程IO技术的奥秘。
    的头像 发表于 09-06 17:22 249次阅读
    远程<b class='flag-5'>IO</b>:<b class='flag-5'>实现</b>设备间高效通信与控制的桥梁