IO基础整理

1. 基础概念

1.1 channel

channel 是一个通道,是I/O操作的桥梁,网络数据通过 channel 进行读写。channel 与 流的区别是:channel 是双向的,既可以从通道中读取数据,又可以从通道中写入数据,流是单向的,只在一个方向上移动;channel 可以进行异步的读写;channel 中的数据先到缓冲区,或者要从缓冲区中写入。

1.2 buffer

数据缓冲区,可以理解为一个内存数组,通过capacity,limit,position变量对读写操作进行控制。可用allocation()方法进行初始化,put() 填充数据,flip() 切换到读模式,此时,limit是实际写入的数据的数量,position为0,用get()方法读取buffer中的内容,clear()清空buffer,rewind()充值position为0.

1.3 selector

selector 一般被称作选择器,或多路复用器,是NIO 的核心组件,可同时注册一个或多个channel,检查通道的状态是否可进行R/W操作。

2. 文件IO

2.1 普通IO

普通IO是面向流的,每次从流中读取一个或多个字节,直至读取到所有字节,流中的数据不能被前后移动。流式IO 有基础IO,如FileOutputStreamFileInputStream,还有buffer IO,如 BufferedOutputStreamBufferdInputStream,其中Buffer IO 更快,因为 基础IO 是够4KB从缓存写入磁盘,而 buffer IO 是满8KB 从缓存写入磁盘,system call(80中断) 次数相对较少。

2.2 NIO

NIO 是面向缓冲区的。缓冲区指的是内存中预留指定大小的存储空间对I/O数据进行临时存储,可以看作是channel(通道)与client/server 的中转站,数据写入channel 或从 channel 中读取,提高I/O效率。

ByteBuffer 的属性:

  • position,下一个要被读取的或者被写入的元素的索引,即偏移量
  • limit,缓冲区的当前终点,不能对超过缓冲区大小的位置进行读写操作
  • capacity,缓冲区的容量,初始化时被设定且不能改变

ByteBuffer 的初始化:

1.ByteBuffer buf = ByteBuffer.allocateDirect(10);	//在JVM堆外中北10个大小的缓冲区
2.ByteBuffer buf = ByteBuffer.allocate(10);	//在JVM堆里准备10个大小的缓冲区

RandomAccessFile 支持随机访问文件,既可以读文件,也可以写文件。通过system call 让data进入pagecache(缓存页,kernel折中方案)。

3. 网络IO

3.1 网络通信TCP

TCP是面向连接的,可靠的传输协议。

3.1.1 三次握手

TCP client 与 server 端建立连接需要三次握手,这个过程中,是内核级开辟资源的过程。成功建立连接之后,即使不低啊用accept ,也会开辟资源,为连接分配一个唯一的 socket 四元组,以 client 和 server 端的 IP 以及 port 组成,即:AIP_CPORT + XIP_XPORT,其中,client 端可以创建 65535 个连接,server socket 可通过 backLog 设置开启client的个数,设置为 2 时,可以开启 3 个 client 连接。可通过 ifconfig 查看 MTU(数据包的大小)。
三次握手过程:(简单描述,不是具体的数据包指令)

  • client 端发起连接请求,syn
  • server 端收到连接请求,发送响应,ack + syn
  • client 端收到响应,进行连接, syn

3.2.2 四次挥手

TCP client 和 server 都可以发起断开连接请求,以下过程以 client 发起断开为例:
四次挥手过程:(简单描述,不是具体的数据包指令)

  • client 发起断开连接请求,FIN,client进入断开请求等待状态,FIN_WAIT2
  • server 端收到断开连接请求,并发送响应,FIN+ACK,client 结束等待状态,close_wait
  • server 端发起断开连接,FIN,server 断开连接,close
  • client 端收到断开连接响应,ACK,client 会有2MSL的等待状态(time_wait)
    client 没有直接 close 的原因是有可能 ack 没有到达,多留一会资源,在这个过程中,会消耗socket四元组资源,这个资源不能被相同对端使用,这个连接名额是被浪费掉的。

3.2 网络IO模型

Linux 的内部结构可以分为3部分,从底层到上层依次是:硬件 ——> 内核空间(内核态) ——> 用户空间(用户态)。为了操作系统的安全性等,进程对IO设备的操作必须通过system call 请求 kernel 协助,kernel会为每个 I/O设备维护一个buffer。用户进程发起请求以后,kernel 接收到请求,然后从 I/O 设备中获取数据到 buffer中,再把数据从buffer 中 copy 到用户进程的地址空间中,进程读取到数据后再对客户端进行响应。在这个过程中,需要花费时间的阶段,一是 数据先通过I/O设备输入至kernel中的buffer上,二 从buffer copy 到进程中,这两个阶段 Linux 有以下几种 IO 模型。
注意:在linux中,所有的程序都可用文件标识符表示,即一切皆文件,以下涉及到的 fd 表示的是对应的文件标识符。

3.2.1 BIO(Blocking I/O)

①. BIO 是阻塞多线程模式的,具体过程如下:

  • server 端(用户进程)启动一个serverSocket,socket=fd3 并绑定端口bind(fd3,8090),然后在该端口创建监听listen(fd3)
  • server 端阻塞,等待 client 连接(等待client 调用accept方法)
  • client 创建连接,server 端 clone 一个线的线程,并阻塞等待 client 发送数据。

②. 问题

  • client 端每创建一个连接,都需要创建一个独立的新线程,并发数量较大时,创建的大量的线程,系统资源占用很大。
  • 阻塞,server 端等待 client 连接阻塞,client 端连接以后,若没有数据到达,线程会阻塞等待 client 端发送数据,造成线程资源的浪费。

3.2.2 NIO(Non-Blocking I/O)

①. NIO 是非阻塞单线程模式的,具体过程如下:

  • server 端(用户进程)启动一个serverSocket, socket=fd3 并绑定端口bind(fd3,8090),然后在该端口创建监听listen(fd3)
  • server 端可以设置 fd3 这个 socket 为 非阻塞(nonblocking),不会阻塞等待client连接,即accept(fd3)不会阻塞
  • 循环访问,查看是否有 client 连接,若 client 端创建连接,accept 有连接时,java 返回连接的 client,linux 返回连接的文件描述符 fd4;若没有 client 连接时,java 返回 null ,linux 返回 -1
  • client 创建连接后,server 端循环访问,查看client端是否有数据发送

②. 在NIO的模式中,server 端会占用两个 socket 资源,其中一个是用于连接的 socket ,另一个是用于传输的socket。

③. 优势
两个 socket 在循环查询连接状态和传输状态时,是非阻塞的,可以通过一个或多个线程,解决n个IO连接的处理。

④. 问题

  • 每次连接都需要进行system call,每次system call 都会通过有软中断(80中断),用户态和内核态进行现场保护,保存当前状态
  • 每一次的循环,都是o(n) 的复杂度,会产生很多无意义的调用,浪费内存资源,这里的read本身是不可避免的,但是,有一些无用的read调用,是不需要掉起的。

3.2.3 多路复用IO(I/O Multiplexing)

①. 多路指的是多条IO,所谓复用,指的是一条调用,可以获取到多条IO的状态。

②. 多路复用与 NIO 的区别是:

  • NIO 需要全量遍历,遍历的成本是用户态内核态的切换实现
  • 多路复用,是通过一个调用获取多条IO的状态,然后程序对有状态的IO进行R/W(select、poll、epoll)

③. 先介绍几个概念

  • 同步:用户进程自己进行 R/W 操作
  • 异步:kernel 完成 R/W 操作
  • 阻塞:blocking
  • 非阻塞:nonblocking
  • 同步阻塞:用户进程自己读取,调用了方法阻塞等待有效结果返回
  • 同步非阻塞:用户进程自己读取,调用了方法,直接返回结果(是否读取到数据的结果),循环去读取
  • 异步非阻塞:linux 目前没有通过 kernel 的异步处理

④. 多路复用机制

  • select

    具体过程,用户进程调用对应socket的select,遍历访问 select 负责的所有socket,找到有状态的fd,recv(fd;其中,在调用 select 的过程是O(1)的时间复杂度,遍历得到有状态的fd是O(m)的时间复杂度,m指的是select负责的socket中有m个fd有状态。

    弊端
    1.每次遍历所有的IO,询问fd的状态,触发一次system call,在用户态内核态的切换过程中,select 负责的fds都需要作为参数传给kernel,fds重复传递;每次kernel被调用后,对fds的状态的更改都需要全量的遍历。
    2.select 单个进程打开的fd是有限制的,有FDSETSIZE设置,32几位默认是1024个,64机位默认是2048个。

  • poll
    poll 的过程与 select 相同,区别是 poll 没有 fd 个数的限制,因为 poll 是基于链表存储的。

  • epoll
    任意时间,用户进程询问kernel中的 fd 时,都是由R/W操作的,会有状态返回,epoll 与 select/poll 的区别在于,select/poll 每次都会将这些 fd 在内核态和用户态复制,而 epoll 是开辟空间来维护这些fd,避免了 fd 的重复拷贝。

具体过程:
首先用户进程开启serverSocket,socket(fd4),并绑定端口,用bind,然后创建监听listen(fd4);
然后开辟内存空间 epoll_create(fd6),这块内存空间,在内核中,采用红黑树+链表的数据结构进行存储;
用 epoll_ctl(fd6,ADD,fd4)【fd6指的是create开辟的内核空间,ADD指的是事件的类型,fd4指的是要监听的fd】来维护这块内存空间,fd6 添加到红黑树中,有状态的放入链表中,有事件注册时,调用 epoll_wait,遍历fds,将有状态的结果集及时取走,避免了 select/poll 的全量遍历问题。

优点
1.没有最大并发连接的限制
2.提升效率,不需要进行全量遍历,只遍历有状态的fd
3.开辟了内存空间维护fd,不需要进行内存拷贝,减少copy的开销

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值