不同网络I/O模型的原理
目录
1、I/O的介绍
1.1、I/O 操作分类
1.2、I/O操作流程阶段
1.3、I/O分类
2、同步I/O
2.1、阻塞I/O
2.2、非阻塞I/O
2.3、I/O复用
2.4、信号驱动式I/O
3、异步I/O
前言
在网络I/O之中,I/O操作往往会涉及到两个系统对象,一个是用户空间调用I/O的进程或者线程,另一个是内核空间的内核系统,当发生I/O操作时,会经历以下两个阶段:
1. 等待数据准备就绪
2. 将数据从内核拷贝到进程或线程中
因为在以上两个阶段上各有不同的情况,所以出现了多种I/O模型。
如下图所示:
关于内核态和用户态的介绍,可参考:操作系统的内核态和用户态场景-CSDN博客
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:在Linux中,socket是一种用于网络通信的抽象接口,提供了数据传输的机制。socket可以被看作是一个端点,允许进程通过它进行数据的发送和接收。
流的抽象:在Linux中,socket被视为一种流(例如TCP流),它允许进程以流式方式读写数据。对于TCP协议来说,数据流是有序的、可靠的。
流的抽象可以理解为像流水一样的数据传输方式,强调了数据传输的连续性和按序性,而不关心数据的具体格式。当使用socket进行网络通信时,网络层和传输层只处理数据的流动,应用层则负责数据的解析和处理。
I/O操作:对socket的读写操作实际上是对网络流的操作。当进程发起对socket的读取操作时,内核会检查缓冲区是否有数据可用。如果有,数据会被传递给进程;如果没有,进程会被阻塞直到数据到达。
2、同步I/O
2.1、阻塞I/O
也称为BIO(Blocking I/O)。
1. 核心原理
阻塞式 I/O:每个连接由一个线程处理,线程在读写数据时会阻塞,直到数据准备好。
典型模型:一请求一线程(One Thread Per Connection)。
底层机制:基于 InputStream
和 OutputStream
的阻塞操作。
整体原理图如下所示:
流程如下:
第一步通常涉及等待数据从网络中到达,当所有等待分组到达时,它被复制到内核中的某个缓冲区。
第二步是把数据从内核缓冲区复制到应用程序缓冲区。
在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. 缺点
- 性能瓶颈:高并发时,线程数激增,占用大量内存和 CPU,容易导致资源耗尽。
- 扩展性差:不适合处理成千上万的并发连接。
5. 适用场景
- 低并发场景:如小型客户端/服务器应用(如命令行工具、简单的 HTTP 服务)。
- 简单协议处理:对性能要求不高的场景。
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操作,将数据从内核拷贝到用户进程。
⚠️注意:
- 当客户处理多个描述符时,必须使用I/O复用;
- 如果一个TCP服务器既要处理监听套接字,又要处理已连接的套接字,一般就要使用I/O复用;
- 如果一个服务器既要处理TCP,又要处理UDP,一般就要使用I/O复用。
- 如果一个服务器要处理多个协议或多个服务,一般就需要使用I/O复用。
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模型、I/O复用(多路转接)和信号驱动式I/O模型都是同步I/O,第二步从数据从内核态copy到用户态的I/O操作(recvfrom)将进程阻塞。
只有异步I/O模型是异步I/O。
参考文章:
1、【编程基础知识】网络I/O模型详解:从阻塞到异步_网络编程 阻塞-CSDN博客
2、五种I/O模型详解-CSDN博客
3、【网络编程下】五种网络IO模型-CSDN博客