带你整理面试过程中关于IO 模型的相关知识

Posted 南淮北安

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带你整理面试过程中关于IO 模型的相关知识相关的知识,希望对你有一定的参考价值。

文章目录

一、基础知识

(1)内核空间和用户空间

系统调用将 Linux 整个体系分为用户态和内核态(或者内核空间和用户空间)

内核态:运行操作系统的程序,控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行

用户态:运行用户程序;

为了使应用程序访问到内核管理的资源例如CPU,内存,I/O;内核必须提供一组通用的访问接口,这些接口就叫系统调用

内核态和用户态最大的区别就是权限不同, 用户态的进程能够访问的资源受到了极大的控制,而运行在内核态的进程可以“为所欲为”。

一个进程可以运行在用户态也可以运行在内核态,那它们之间肯定存在用户态和内核态切换的过程。

  • 系统调用,这个上面已经讲解过了,
  • 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换。例如:缺页异常。
  • 外设中断:当外设完成用户的请求时,会向CPU发送中断信号。

消耗其实主要发生在用户态和内核态之间的切换,因为切换时需要将用户态的CPU状态保存下来,然后切换到内核态执行,得到结果后又要切换回用户态。

(2)文件描述符

文件描述符(File descriptor):是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。

当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。

但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

(3)直接 I/O 和缓存 I/O

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。

在 Linux 的缓存 I/O 机制中,以write为例,数据会先被拷贝进程缓冲区,在拷贝到操作系统内核的缓冲区中,然后才会写到存储设备中。

缓存I/O的write:

直接I/O的write:(少了拷贝到进程缓冲区这一步)

二、阻塞I/O模型

阻塞I/O模型是常见的I/O模型,在读写数据时客户端会发生阻塞。

阻塞I/O模型的工作流程为:在用户线程发出I/O请求之后,内核会检查数据是否就绪,此时用户线程一直阻塞等待内存数据就绪;

数据是否就绪:需要把数据从磁盘复制到缓冲区

在内存数据就绪后,内核将数据复制到用户线程中,并返回I/O执行结果到用户线程,此时用户线程将解除阻塞状态并开始处理数据。

将数据从内核缓冲区拷贝到用户缓冲区

典型的阻塞I/O模型的例子为data = socket.read(),如果内核数据没有就绪,Socket 线程就会一直阻塞在read()中等待内核数据就绪。

三、非阻塞I/O模型

非阻塞I/O模型指用户线程在发起一个I/O操作后,无须阻塞便可以马上得到内核返回的一个结果。

如果内核返回的结果为false,则表示内核数据还没准备好,需要稍后再发起I/O操作。

一旦内核中的数据准备好了,并且再次收到用户线程的请求,内核就会立刻将数据复制到用户线程中并将复制的结果通知用户线程。

在非阻塞I/O模型中,用户线程需要不断询问内核数据是否就绪,在内存数据还未就绪时,用户线程可以处理其他任务,在内核数据就绪后可立即获取数据并进行相应的操作

典型的非阻塞I/O模型一般如下:

四、多路复用I/O模型

多路复用I/O模型是多线程并发编程用得较多的模型,又称为事件驱动模型

在多路复用I/O模型中会有一个被称为Selector的线程不断轮询多个Socket的状态,只有在Socket有读写事件时,才会通知用户线程进行I/O读写操作。

所以又可称为事件驱动,当有Socket可读或者可写时,操作系统会给一个通知,主要是通过select/poll/epoll 之类的调用函数

因为在多路复用I/O模型中只需一个线程就可以管理多个Socket(阻塞I/O模型和非阻塞1/O模型需要为每个Socket都建立一个单独的线程处理该Socket上的数据),并且在真正有Socket读写事件时才会使用操作系统的I/O资源,大大节约了系统资源。

比如教室有10名学生和1名老师,学生上课不停的提问,一个老师无法处理这么多问题,学校于是就为每个学生都分配了一名老师,这就是单路复用,一名学生对应一名老师
后来有一天来了一名超能力老师,这位老师回答问题非常迅速,并且可以应对所有的问题。而且这位老师采用的是学生提问先举手,确认举手学生后,再回答问题。这就是多路复用

多路复用I/O模型通过一个线程管理多个Socket通道,在Socket有读写事件触发时才会通知用户线程进行I/O读写操作。因此,多路复用I/O模型在连接数众多且消息体不大的情况下有很大的优势

非阻塞I/O模型在每个用户线程中都进行Socket状态检查,而在多路复用I/O模型中是在系统内核中进行Socket状态检查的,这也是多路复用I/O模型比非阻塞I/O模型效率高的原因

多路复用I/O模型通过在一个Selector线程上以轮询方式检测在多个Socket上是否有事件到达,并逐个进行事件处理和响应。

因此,对于多路复用I/O模型来说,在事件响应体(消息体)很大时,Selector线程就会成为性能瓶颈,导致后续的事件迟迟得不到处理,影响下一轮的事件轮询。

在实际应用中,在多路复用方法体内一般不建议做复杂逻辑运算,只做数据的接收和转发,将具体的业务操作转发给后面的业务线程处理。

1. select

select系统调用是让我们的程序监控多个文件描述符的状态变化的

它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。

所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

缺点:有最大连接数限制,时间复杂度大 O(n)

2. poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有 fd 后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历 fd(文件描述符)

这个过程经历了多次无谓的遍历。

但是它没有最大连接数的限制,原因是它是基于链表来存储的

缺点:
(1)事件复杂度为 O(n)
(2)大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义

因为可能大量的 fd,只有个别设备就绪甚至没有设备就绪

3. epoll

epoll是poll的改进版本,解决了 select 和 poll 的所有缺点

(1)没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
(2)数据拷贝少:只需要在合适的时候调用 EPOLL_CTL_ADD 将文件描述符拷贝到内核中,这个操作并不频繁 (而select/poll每次都需要循环的拷贝)
(3)事件回调机制,避免使用遍历主动去知道,而是使用回调函数的方法,为每个fd指定一个回调函数,只有活跃的socket才会主动调用回调函数。这个操作时间复杂度O(1),即使文件描述符数目很多,效率也不会收到影响

epoll 有两种触发方式:LT(水平触发)、ET(边缘触发)
水平触发:只要存在事件就会不断的触发,直到处理完成
边缘触发:只触发一次相同事件,或者说在非触发到触发两个状态转换时才触发

表面上看 epoll 的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比 epoll 好,毕竟 epoll 的通知机制需要很多回调函数

五、信号驱动I/O模型

在信号驱动I/O模型中,在用户线程发起一个I/O请求操作时,系统会为该请求对应的Socket注册一个信号函数,然后用户线程可以继续执行其他业务逻辑;在内核数据就绪时,系统会发送一个信号到用户线程,用户线程在接收到该信号后,会在信号函数中调用对应的I/O读写操作完成实际的I/O请求操作。

六、异步I/O模型

在异步I/O模型中,用户线程会发起一个异步 read 操作到内核,内核在接收到异步 read 请求后会立刻返回一个状态,来说明请求是否成功发起,在此过程中用户线程不会发生任何阻塞。

接着,内核会等待数据准备完成并将数据复制到用户线程中,在数据复制完成后内核会发送一个信号到用户线程,通知用户线程异步读操作已完成。

在异步I/O模型中,用户线程不需要关心整个 I/O 操作是如何进行的,只需发起一个请求,在接收到内核返回的成功或失败信号时说明 I/O 操作已经完成,直接使用数据即可

在异步I/O模型中,I/O操作的两个阶段(请求的发起、数据的读取)都是在内核中自动完成的,最终发送一个信号告知用户线程I/O操作已经完成,用户直接使用内存写好的数据即可,不需要再次调用I/O函数进行具体的读写操作,因此在整个过程中用户线程不会发生阻塞。

在信号驱动模型中,用户线程接收到信号便表示数据已经就绪,需要用户线程调用I/O函数进行实际的I/O读写操作,将数据读取到用户线程;而在异步I/O模型中,用户线程接收到信号便表示I/O操作已经完成(数据已经被复制到用户线程),用户可以开始使用该数据了。

异步I/O需要操作系统的底层支持,在Java 7中提供了AsynchronousI/O操作。

七、BIO(同步阻塞 I/O 模型)

blocking IO

数据的读取写入必须阻塞在一个线程内等待其完成。

这里使用那个经典的烧开水例子,这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择。

八、NIO(同步非阻塞 I/O 模型)

new IO

关于 Java NIO 的学习:一篇文章带你搞定 Java 高并发中的网络 NIO

Java NIO的实现主要涉及三大核心内容:Selector(选择器)、Channel(通道)和Buffer(缓冲区)。


选择器:用于检测在多个注册的通道上是否有I/O事件发生,并对检测到的I/O事件进行相应的响应和处理。因此通过一个Selector线程就可以实现对多个Channel的管理,不必为每个连接都创建一个线程,避免线程资源的浪费和多线程之间的上下文切换导致的开销。而且选择器只有在通道上有读写事件发生时,才会调用I/O函数进行读写操作,可极大减少系统开销,提高系统的并发量。
通道:通道 和 I/O 中的流类似,只不过流是单向的(分为输入流和输出流),而通道是双向的,既可以用来进行读操作,也可以用来进行写操作
缓冲区:缓冲区实际上是一个容器,其内部通过一个连续的字节数组存储I/O上的数据。在NIO中,通道在文件、网络上对数据的读取或写入都必须经过缓冲区。

如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。

非阻塞I/O模型中的Selector线程通常将I/O的空闲时间用于执行其他通道上的I/O操作,所以一个Selector线程可以管理多个输入和输出通道

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

九、AIO(异步非阻塞 I/O 模型)

Asynchronous IO

异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。

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

十、知识补充

1. 非阻塞 I/O 系统调用和异步 I/O 系统调用的区别

一个非阻塞I/O 系统调用 read() 操作立即返回的是任何可以立即拿到的数据, 可以是完整的结果, 也可以是不完整的结果, 还可以是一个空值

而异步I/O系统调用 read() 结果必须是完整的, 但是这个操作完成的通知可以延迟到将来的一个时间点

2. 多路复用怎么解决阻塞IO存在的问题

阻塞IO模型:在用户线程发出I/O请求之后,内核会检查数据是否就绪,此时用户线程一直阻塞等待内存数据就绪;等待的过程其实比较耗费系统资源,而且一次只能监控一个socket,当一个socket阻塞时就不能处理其他socket请求

比如:一个教室有一名助教,它只能管理一名学生,助教负责解决学生的疑问,学生提问一个问题了,助教需要思考才能回答,这个过程中,学生需要一直站着等着。

存在的问题就是一个线程只能监控一个socket

而多路复用,通过一个线程监控多个socket,而且是基于事件驱动只有在Socket有读写事件触发时才会通知用户线程进行I/O读写操作

同样刚才那个例子,现在相当于一个专业老师管理多个学生,学生提问需要举手,老师确定学生举手了再对学生的问题回答,而且回答速度很快,可以满足多个学生的需求。

具体是通过 select/poll/epoll 常用的IO多路复用函数实现的

3. NIO 和传统 I/O 的区别

(1)I/O是面向流的,NIO是面向缓冲区的

在面向流的操作中,数据只能在一个流中连续进行读写,数据没有缓冲,因此字节流无法前后移动。
而在NIO中每次都是将数据从一个通道读取到一个缓冲区中,再从缓冲区写入通道中,因此可以方便地在缓冲区中进行数据的前后移动等操作。
该功能在应用层主要用于数据的粘包、拆包等操作,在网络不可靠的环境下尤为重要。

(2)传统I/O的流操作是阻塞模式的,NIO的流操作是非阻塞模式的。

在传统I/O下,用户线程进行I/O读写操作时,该线程将一直被阻塞,直到数据被读取或数据完全写入。
NIO通过选择器监听通道上事件的变化,在通道上有数据发生变化时通知该线程进行读写操作。

(3)传统I/O 没有选择器,而 NIO 中有选择器

以上是关于带你整理面试过程中关于IO 模型的相关知识的主要内容,如果未能解决你的问题,请参考以下文章

带你整理面试过程中关于 Java 的内存模型 JMM(Java Memory Model)的相关知识

带你整理面试过程中关于ThreadLocal的相关知识

带你整理面试过程中关于锁的相关知识点下

带你整理面试过程中关于ARP 协议的相关知识点

带你整理面试过程中关于多线程中的线程池的相关知识点

带你整理面试过程中关于 Java 中的 异常分类及处理的相关知识