• 关于IO


    前言

    IO在计算机中指Input/Output,也就是输入和输出。由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。IO指的就是读入/写出数据的过程,和等待读入/写出数据的过程。一旦拿到数据后就变成了数据操作了,就不是IO了。

    拿网络IO来说,等待的过程就是数据从网络到网卡再到内核空间。读写的过程就是内核空间和用户空间的相互拷贝。所以IO就包括两个过程,一个是等待数据的过程,一个是读写(拷贝)数据的过程。而且还要明白,一定不能包括操作数据的过程。

    不管是网络IO还是磁盘IO,对于读操作而言,都是等到网络的某个数据分组到达后/数据准备好后,将数据拷贝到内核空间的缓冲区中,再从内核空间拷贝到用户空间的缓冲区

    磁盘IO主要的延时是由(以15000rpm硬盘为例):
    机械转动延时(机械磁盘的主要性能瓶颈,平均为2ms) + 寻址延时(2~3ms) + 块传输延时(一般4k每块,40m/s的传输速度,延时一般为0.1ms)
    决定。(平均为5ms)而网络IO主要延时由:
    服务器响应延时 + 带宽限制 + 网络延时 + 跳转路由延时 + 本地接收延时
    决定。(一般为几十到几千毫秒,受环境干扰极大)

    以下内容主要为unix相关

    1. 操作系统 IO

    1.1 操作系统 IO 模型

    高级语言(Java,C#)中的很多操作如文件操作,网络操作,内存操作,线程操作,I/O操作等,都不是高级语言自身能够实现的。

    也不是它们的虚拟机(JVM,CLR)能够实现的,实际最终是由操作系统实现的,因为这些都是系统资源,只有操作系统才有权限访问。

    如果你用Java或C#代码创建了一个文件,千万不要以为是Java或C#创建了这个文件,它们只是层层向下调用了操作系统的API,然后到文件系统API,最后可能到磁盘驱动程序。

    所以了解Java高级程序的IO就需要底层操作系统中的IO模型。操作系统的IO模型主要分为五种。

    1. 传统的阻塞 I/O(Blocking I/O)
      阻塞IO
      两阶段全程阻塞。两阶段:系统调用获取数据、将数据从内核复制到用户缓冲区。

    2. 非阻塞 I/O(Non-blocking I/O)
      非阻塞IO
      两个阶段是这样的:第一阶段是非阻塞的不断检查是否数据准备好 (比方说每隔1分钟询问,对于程序来说是非阻塞的占用CPU资源),第二阶段阻塞读取数据

    每次应用程序询问内核是否有数据准备好。如果就绪,就进行拷贝操作;如果未就绪,就不阻塞程序,内核直接返回未就绪的返回值,等待用户程序下一个轮询。

    在这两个阶段中,用户进程只有在数据复制阶段被阻塞了,而等待数据阶段没有阻塞,但是用户进程需要盲等,不停地轮询内核,看数据是否准备好。

    1. I/O 多路复用(I/O multiplexing)
      IO复用
      多路复用一般都是用于网络IO,服务端与多个客户端的建立连接。

    相比于阻塞IO模型,多路复用只是多了一个select/poll/epoll函数。select函数会不断地轮询自己所负责的文件描述符/套接字的到达状态,当某个套接字就绪时,就对这个套接字进行处理。select负责轮询等待,recvfrom负责拷贝。当用户进程调用该select,select会监听所有注册好的IO,如果所有IO都没注册好,调用进程就阻塞。

    对于客户端来说,一般感受不到阻塞,因为请求来了,可以用放到线程池里执行;但对于执行select的操作系统而言,是阻塞的,需要阻塞地等待某个套接字变为可读。

    IO多路复用其实是阻塞在select,poll,epoll这类系统调用上的,复用的是执行select,poll,epoll的线程。

    1. 异步 I/O(Asynchronous I/O)
      异步IO
      两阶段都是非阻塞

    用户进程发起系统调用后,立刻就可以开始去做其他的事情,然后直到I/O数据准备好并复制完成后,内核会给用户进程发送通知,告诉用户进程操作已经完成了。

    异步I/O执行的两个阶段都不会阻塞读写操作,由内核完成。
    完成后内核将数据放到指定的缓冲区,通知应用程序来取。

    1. 信号驱动 的 I/O(Signal Driven I/O)
    2. 信号驱动IO
      当数据报准备好的时候,内核会向应用程序发送一个信号,进程对信号进行捕捉,并且调用信号处理函数来获取数据报。

    第一阶段构造一个信号处理器,第二阶段阻塞读取数据

    • 数据准备阶段:未阻塞,当数据准备完成之后,会主动的通知用户进程数据已经准备完成,对用户进程做一个回调。
    • 数据拷贝阶段:阻塞用户进程,等待数据拷贝。
    对比

    同步&异步

    同步和异步是针对应用程序和内核交互而言的,也可理解为被调用者(操作系统) 的角度来说。
    同步是用户进程触发IO操作并等待或轮询的去查看是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知,需要CPU支持。

    从同步和异步来说,只有异步IO模型是异步的,其他均为同步。

    阻塞&非阻塞

    阻塞和非阻塞是针对于进程在访问数据的时候,也可理解为调用者(程序)角度来说。根据IO操作的就绪状态来采取的不同的方式。
    阻塞方式下读取或写入方法将一直等待,而非阻塞方式下读取或写入方法会立即返回一个状态值。

    1.2 操作系统层实现的 IO 调用函数

    recvfrom

    Linux系统提供给用户用于接收网络IO的系统接口。从套接字上接收一个消息,可同时应用于面向连接和无连接的套接字。

    如果此系统调用返回值 < 0,并且 errno 为 EWOULDBLOCK或EAGAIN(套接字已标记为非阻塞,而接收操作被阻塞或者接收超时 )时,连接正常,阻塞接收数据(这很关键,前4种IO模型都设计此系统调用)。

    select

    select系统调用允许程序同时在多个底层文件描述符上,等待输入的到达或输出的完成。以数组形式存储文件描述符,64位机器默认2048个。当有数据准备好时,无法感知具体是哪个流OK了,所以需要一个一个的遍历,函数的时间复杂度为O(n)。

    select 不足的地方:

    1 每次 select 都要把全部 IO 句柄复制到内核

    2 内核每次都要遍历全部 IO 句柄,以判断是否数据准备好

    3 select 模式最大 IO 句柄数是 1024,太多了性能下降明显

    poll

    以链表形式存储文件描述符,没有长度限制。本质与select相同,函数的时间复杂度也为O(n)。

    epoll

    是基于事件驱动的,如果某个流准备好了,会以事件通知,知道具体是哪个流,因此不需要遍历,函数的时间复杂度为O(1)。

    1. 每次新建IO句柄(epoll_create)才复制并注册(epoll_register)到内核
    2. 内核根据IO事件,把准备好的IO句柄放到就绪队列
    3. 应用只要轮询(epoll_wait)就绪队列,然后去读取数据

    只需要轮询就绪队列(数量少),不存在select的轮询,也没有内核的轮询,不需要多次复制所有的IO句柄。因此,可以同时支持的IO句柄数轻松过百万。

    sigaction

    用于设置对信号的处理方式,也可检验对某信号的预设处理方式。Linux使用SIGIO信号来实现IO异步通知机制。

    2. 框架语言中的 IO 的实现

    前面说到高级语言的IO是依赖于操作系统的IO的。高级语言中的IO就是对IO进一步的封装。

    2.1 JDK IO NIO

    2.1.1 BIO 同步阻塞编程

    JDK1.4之前常用的编程方式。

    实现过程:
    server 端创建 ServerSocket 开启端口监听网络请求,客户端启动 socket 发起网络请求连接服务端。服务端创建连接线程处理请求,不停的监听socket中有没有数据。如果服务端没有线程可用,客户端则会阻塞等待或遭到拒绝,并发效率比较低。

    使用场景:BIO适用于连接数目比较小且固定的架构,对服务器资源要求高,并发局限于应用中。

    造成的问题一是服务端可能创建很多的线程二是服务端的CPU 被占用阻塞等待数据, 无事可干,浪费CPU资源。可以使用线程池改善。

    2.1.2 NIO 同步非阻塞编程

    NIO 本身是基于事件驱动思想来完成的,当 socket 有流可读或可写入时,操作系统会相应地通知应用程序进行处理,应用再将流读取到缓冲区或写入操作系统。一个有效的请求对应一个线程,当连接没有数据时,是没有工作线程来处理的。

    服务器实现模式为一个请求一个通道,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。

    使用场景
    NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程复杂,JDK1.4 开始支持。

    JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%。而且JDK中NIO的api设计复杂,通常使用Netty框架进行开发。

    2.1.3 AIO 异步非阻塞

    进行读写操作时,只须直接调用api的read或write方法即可。一个有效请求对应一个线程,客户端的IO请求都是OS先完成了再通知服务器应用去启动线程进行处理。

    使用场景
    AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK1.7 开始支持。

    2.2 Netty NIO

    前面说到Netty是对NIO 进行封装。具体可看本人另外一篇博客,不再赘述。Netty入门

    References

  • 相关阅读:
    wireshark和tcpdump抓包TCP乱序和重传怎么办?PCAP TCP排序工具分享
    途游斗地主加密协议分析及破解
    linux下unzip解压报错“symlink error: File name too long”怎么办?提供解决方案。
    GPS NMEA-0183协议常用报文数据格式
    微信出现“已停止访问该网页”或“关于潜在的违法或违规内容”怎么办?如何获取被屏蔽的网页的网址?...
    m3u8视频格式分析
    精确哈克,以贪婪为基础的欺诈式引流法
    某米浏览器黑名单文件破解
    HTTP.sys远程执行代码漏洞
    SNMP协议利用
  • 原文地址:https://www.cnblogs.com/wei57960/p/13170963.html
Copyright © 2020-2023  润新知