java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > 不同网络I/O模型的原理

不同网络I/O模型的原理分析

作者:找不到、了

这篇文章主要介绍了不同网络I/O模型的原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

在网络I/O之中,I/O操作往往会涉及到两个系统对象,一个是用户空间调用I/O的进程或者线程,另一个是内核空间的内核系统,当发生I/O操作时,会经历以下两个阶段:

1. 等待数据准备就绪

2. 将数据从内核拷贝到进程或线程中

因为在以上两个阶段上各有不同的情况,所以出现了多种I/O模型。

如下图所示:

1、I/O的介绍

I/O 操作(Input/Output Operation)指的是计算机系统中与外部环境(如网络、磁盘、键盘、显示器等)进行数据交换的过程。

1.1、I/O 操作分类

1.网络输入(接收数据):

如 recv() 或 read() 函数,用于从网络连接中读取数据。

2.网络输出(发送数据):

如 send() 或 write() 函数,用于向网络连接发送数据。

3.网络连接管理:

如 accept() 函数,用于接受来自客户端的连接请求。

4.网络监听:

如 listen() 函数,用于监听进入的连接请求。

1.2、I/O操作流程阶段

通常用户进程中的一个完整I/O分为两个阶段:

用户进程空间→内核空间 ,内核空间→设备空间;

整体模型图所示:

1.3、I/O分类

I/O分为内存I/O、网络I/O和磁盘I/O三种。

1、内存I/O

涉及直接与计算机内存进行数据交换,如读取和写入内存中的数据。

2、网络I/O

涉及通过网络接口进行的数据交换,例如从服务器下载文件或向服务器发送请求。

3、磁盘I/O

涉及与存储设备(如硬盘驱动器或固态硬盘)进行数据交换,例如读取和写入磁盘上的文件。

Linux中进程无法直接操作I/O设备,其必须通过系统调用请求内核来协助完成I/O操作。 内核会为每个I/O设备(例如硬盘、网络接口)维护一个缓冲区,缓冲区是内存中的一块区域,用于临时存储数据,以提高I/O操作的效率。

对于一个输入操作来说,进程I/O系统调用(如 read())读取数据时,它会先看为该设备维护的缓冲区,看看是否已经有待处理的数据。没有的话再到设备(比如网卡设备)中读取(因为设备I/O一般速度较慢,需要等待)。

如果缓冲区中没有数据,内核会发起设备I/O操作,从设备(例如网络接口卡、磁盘等)中读取数据。由于设备I/O操作通常较慢,需要时间来完成,内核会先等待数据到达设备缓冲区,再进行处理。

如下图所示:

所以,对于一个网络输入操作通常包括两个不同阶段:

1、等待网络数据到达网卡,把数据从网卡读取到内核缓冲区,准备好数据。

2、从内核缓冲区复制数据到用户进程空间。

注意:

小结

流的抽象可以理解为像流水一样的数据传输方式,强调了数据传输的连续性和按序性,而不关心数据的具体格式。当使用socket进行网络通信时,网络层和传输层只处理数据的流动,应用层则负责数据的解析和处理。

I/O操作:对socket的读写操作实际上是对网络流的操作。当进程发起对socket的读取操作时,内核会检查缓冲区是否有数据可用。如果有,数据会被传递给进程;如果没有,进程会被阻塞直到数据到达。

2、同步I/O

2.1、阻塞I/O

也称为BIO(Blocking I/O)。

1.核心原理

整体原理图如下所示:

流程如下:

在Linux中,默认情况下,阻塞I/O的所有套接字都是阻塞的。

2.代码示例

// BIO 服务器示例
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept(); // 阻塞,等待连接
new Thread(() -> {
InputStream input = socket.getInputStream();
byte[] buffer = new byte[1024];
int len = input.read(buffer); // 阻塞,等待数据
System.out.println(new String(buffer, 0, len));
}).start();
}

3.优点

4.缺点

5.适用场景

2.2、非阻塞I/O

允许应用程序在数据未准备好时不必等待,可以继续执行其他任务。

(Non-blocking I/O)模型如下所示:

流程如下:

非阻塞的recvform系统调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error(EAGAIN或EWOULDBLOCK)。

进程在返回之后,可以先处理其他的业务逻辑,稍后再发起recvform系统调用。 采用轮询的方式检查内核数据,直到数据准备好。再拷贝数据到进程,进行数据处理。

在Linux下,可以通过设置套接字选项使其变为非阻塞。

总结:

可以看到前三次调用recvfrom请求时,并没有数据返回,内核返回errno(EWOULDBLOCK),并不会阻塞进程。 当第四次调用recvfrom时,数据已经准备好了,于是将它从内核空间拷贝到程序空间,处理数据。

⚠️注意:但是将数据从内核拷贝到用户空间,这个阶段阻塞。

2.3、I/O复用

多路监控:使用select、poll或epoll等系统调用来监控多个I/O流。当其中一个I/O流有数据可读或可写时,系统调用返回。适用于在单个线程内管理多个连接。

(I/O Multiplexing)模型如下所示:

流程如下:

I/O多路复用的好处在于单个进程就可以同时处理多个网络连接的I/O。

它的基本原理是不再由应用程序自己监视连接,而由内核替应用程序监视文件描述符。通过 select、poll、epoll 等机制,允许一个进程同时监视多个文件描述符,当某个文件描述符就绪时再进行 IO 操作。这种模型下,程序可以同时处理多个连接,提高了并发处理能力。

示例:

以select函数为例,当用户进程调用了select,那么整个进程会被阻塞,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好,select就会返回。 这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。

注意:

2.4、信号驱动式I/O

该模型允许socket进行信号驱动I/O,并注册一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

模型如下图所示:

注意:

虽然信号驱动IO在注册完信号处理函数以后,就可以做其他事情了。但是第二阶段拷贝数据的过程当中进程依然是被阻塞的。

3、异步I/O

异步I/O的工作机制:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。

异步I/O与信号驱动I/O模型区别在于:

信号驱动式I/O是有内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。

模型如下图所示:

异步I/O不是按顺序执行。用户进程进行aio_read系统调用之后,就可以去处理其他逻辑了,无论内核数据是否准备好,都会直接返回给用户进程,不会对进程造成阻塞。

这是因为aio_read只向内核递交申请,并不关心有没有数据。 等到数据准备好了,内核直接复制数据到进程空间,然后内核向进程发送通知,此时数据已经在用户空间了,可以对数据进行处理。

总结

同步I/O和异步I/O的比较:

简单的讲:就是是否参与了I/O操作;

前四种I/O模型——阻塞式I/O模型、非阻塞式I/O模型、I/O复用(多路转接)和信号驱动式I/O模型都是同步I/O,第二步从数据从内核态copy到用户态的I/O操作(recvfrom)将进程阻塞。

只有异步I/O模型是异步I/O。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文